likes
comments
collection
share

我应该如何去理解React Context

作者站长头像
站长
· 阅读数 15

前言

React Context是React提供给开发者的一种常用的状态管理机制,利用Context可以有效的将同一个状态在多级组件中进行传递,并能够在状态更新时,自动的通知各个组件进行更新。那React Context又是如何做到这一点的,以及为什么需要这么设计呢?

为什么需要Context

在React的数据管理理念中,一直遵循着单项数据流以及数据不变性的理念。当我们需要从父组件将状态向子组件传递时,我们往往需要通过Props显式进行传递,例如:

const Father:FC = () => {
  const [count, setCount] = useState<number>(0)
  return (
    <Son count={count} />
  )
}

const Son:FC = (props) => {
  const { count } = props;
  return (
    <span>{count}</span>
  )
}


但是,倘若父组件需要向子组件的子组件,也就是孙组件进行状态的传递呢?或者父组件需要同时向多个子组件进行传递呢?当然,继续使用props进行逐层往下的显示传递肯定也是能实现这个需求的,但那样的代码未免过于繁琐且难以维护,如果能够在父组件里维护一个类似于Js里的全局变量,所有的子组件都能使用这个全局变量不就好了吗?是的,这个就是Context的作用,但又远远不止这么简单。

Context是什么?

Context提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。我应该如何去理解React Context

Context如何使用?

创建Context

首先,我们需要在父组件中, 利用React.createContext创建一个React Context对象,这个方法接受一个入参,作为当前Context的默认值。

import React from 'react'
 
const Context = React.createContext(defaultValue)

向下传递数据

利用Context对象返回的Provide组件,包裹需要传递数据的子组件。每个 Context 对象都会返回一个 Provider React 组件,它接收一个 value 属性,可将数据向下传递给消费组件。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。


const Father:FC = () => {
  const [count, setCount] = useState<number>(0)
  return (
    <Context.Provider value={count}>
    	<Son />
    </Context.Provider>
  )
}

接收数据

被包裹的子组件,利用useContext获取父组件传递的数据。

const Son:FC = (props) => {
  const value = React.useContext(Context);
  return (
    <span>{value}</span>
  )
}

Context如何以及为何这样实现?

让我们回到Context使用过程的第一步,通过阅读源码去研究createContext究竟做了什么样的工作?剔除了一些干扰代码,其实createContext做的事情其实非常简单,创建了一个对象,保存了当前context的value, 以及返回了一个Provide组件。

import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';

import type {ReactProviderType} from 'shared/ReactTypes';
import type {ReactContext} from 'shared/ReactTypes';

export function createContext<T>(defaultValue: T): ReactContext<T> {
  // TODO: Second argument used to be an optional `calculateChangedBits`
  // function. Warn to reserve for future use?

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),

    // Add these to use same hidden class in VM as ServerContext
    _defaultValue: (null: any),
    _globalName: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };


  return context;
}

在React编译的过程中,会将我们写的JSX语法代码,转化成React.createElement方法,执行这个方法后,会得到一个ReactElement元素对象,也就是我们所说的Virtual Dom。这个元素对象,会记录着当前组件所接收的入参以及元素类型。而Provide组件实际上编译完之后也是一个ReactElement,只不过他的Type跟正常的组件并不一样,而是context.Provider。

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

那么,子组件又是如何利用Provider和useContext去获取到最新的数据的呢?useContext接收一个context对象作为参数,从context._currentValue中读取当前contetx的value值。


function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {

  // 获取当前context保存的value
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

	// ...do something

  // 返回当前的值
  return value;
}

问题又来了,当父组件的状态改变时,又是如何通过Provider触发更新,通知订阅当前状态的子组件进行重新渲染的呢?当父组件的状态进行更新时,React整体会进入到调度更新阶段,Fiber节点会进入到beginWork的方法当中,在这个方法里面,会根据当前更新节点的类型,从而执行相对应的方法。上文提到,Provider组件是有单独的自己的类型ContextProvider的,所以会进入到相对应的更新方法,updateContextProvide。其实updateContextProvide里做的事情,大抵可以概括为:首先更新context._currentValue, 然后比较新老value是否发生改变,如果没有发生改变,则跳出更新函数,复用当前fiber节点。如果发生了改变,则调用一个叫propagateContextChange的方法,对该Provider组件的子组件进行深度遍历,找到订阅了当前context的子组件,并打上需要更新的标记,lane。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;


  pushProvider(workInProgress, context, newValue);

  if (enableLazyContextPropagation) {
    // In the lazy propagation implementation, we don't scan for matching
    // consumers until something bails out, because until something bails out
    // we're going to visit those nodes, anyway. The trade-off is that it shifts
    // responsibility to the consumer to track whether something has changed.
  } else {
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      if (is(oldValue, newValue)) {
        // No change. Bailout early if children are the same.
        if (
          oldProps.children === newProps.children &&
          !hasLegacyContextChanged()
        ) {
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes,
          );
        }
      } else {
        // The context value changed. Search for matching consumers and schedule
        // them to update.
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }
	// do something...
}

那么, 在深度遍历的时候,又是如何知道当前子组件是否有订阅当前Context的呢?其实在使用useContext的时候,除了读取当前context的value,还会把接收的context对象信息保存在当前组件的Fiber.dependencies上,所以在遍历的时候,只需要看当前组件的dependencies上有没有当前context便可以知道当前组件是否存在订阅关系了。

function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };

    if (lastContextDependency === null) {

      lastContextDependency = contextItem;
      consumer.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        consumer.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

只有被Provider组件包裹的子组件才能读取到Context的状态吗?其实并不是,所有的组件都可以通过useContext去读取Context对象里的currentValue,但是,只有被Provider组件包裹的组件,才能订阅到Context对象里的value的变化,在变化的时候及时的更新自身组件的状态。这样设计的目的,实际上也是为了更好的优化React在更新组件的性能,试想,如果每创建一个Context对象,就默认所有的组件都可以订阅到这个Context的变化,那么整个Fiber树在更新的过程中,需要遍历的Fiber节点就太庞大了,一些完全不需要且没有订阅当前Context的组件也需要被遍历到,这其实是一种性能的浪费。

转载自:https://juejin.cn/post/7235435351049568312
评论
请登录