React Hooks 源码解读之 useContext

1,011 阅读4分钟

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

react 版本:v17.0.3

1、入口

React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:

  • 组件挂载阶段:

    useContext = ReactCurrentDispatcher.current.useContext = HooksDispatcherOnMount.useContext = readContext;

  • 组件更新阶段:

    useContext = ReactCurrentDispatcher.current.useContext = HooksDispatcherOnUpdate.useContext = readContext;

无论是挂载阶段还是更新阶段,useContext 最终执行的函数都是 readContext 。接下来,我们来看看 readContext 的实现。

2、readContext

// packages/react-reconciler/src/ReactFiberHooks.new.js
export function readContext<T>(context: ReactContext<T>): T {
  
  	// 删除了Dev部分代码

  // 以下两个属性是为了适配多平台(浏览器端/移动端)
  // _currentValue
  // _currentValue2

  // ReactDOM 中 isPrimaryRenderer 为 true,定义的就是 true
  // 实际就是一直会返回  context._currentValue
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    // 新建一个 context 链表的节点,节点上存储着传递进来的 context 对象 和 context 对象上的value
    //  next 指针连接下一个 context 项
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };

    if (lastContextDependency === null) {
      
			// 删除了部分代码
      
      // This is the first dependency for this component. Create a new list.
      // 这是组件的第一个依赖项,创建一个新的 context 依赖列表
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        currentlyRenderingFiber.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      // 在链表后面添加一个新的 context 项
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  // readContext最终返回的是context._currentValue
  return value;
}

readContext 把 context 对象上的 _currentValue/_currentValue2 取出来,接着构建一个新的 context项,该 context 项上存储着当前的 context 对象和 context 对象上的 _currentValue/_currentValue2,并通过 next 指针连接下一个 context 项,接着构建一个 context 依赖列表,并将该列表挂载到当前正在渲染的 Fiber 节点,最后返回从 context 对象上取出来的 _currentValue/_currentValue2 。

readContext 接收一个 context 对象 (React.createContext 的返回值) 并返回该 context 的当前值。我们接下来看看这个 context 对象和该 context 的当前值。

3、createContext

React 的 Context 属性实现了组件间跨层级传递 props 。Context 对象通过 createContext 方法创建:

const MyContext = React.createContext(defaultValue);

我们来看看 createContext 的实现:

// packages/react/src/ReactContext.js

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> = {
    // ReactContext中的$$typeof是作为createElement中的属性type中的对象进行存储的
    $$typeof: REACT_CONTEXT_TYPE,
    // 作为支持多个并发渲染器的解决方法,我们将一些渲染器分类为主要渲染器,将其他渲染器分类为辅助渲染器。    
    // As a workaround to support multiple concurrent renderers, we categorize    
    // some renderers as primary and others as secondary.   
    
    // 我们只希望最多有两个并发渲染器:React Native(主要)和Fabric(次要);    
    // React DOM(主要)和React ART(次要)。    
    // 辅助渲染器将自己的context的value存储在单独的字段中。    
    // 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. 
    
    // <Provider value={xxx}>中的value就是赋值给_currentValue的    
    // 也就是说_currentValue和_currentValue2作用是一样的,只是分别给主渲染器和辅助渲染器使用

    _currentValue: defaultValue, // Provider 的value 属性
    _currentValue2: defaultValue, // Provider 的value 属性
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0, // 用来追踪 context 的并发渲染器数量
    // These are circular
    Provider: (null: any),  // 提供组件
    Consumer: (null: any), // 应用组件
  };

  // 给context对象添加 Provider 属性,并且 Provider 中的_context指向的是 context 对象
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;

  if (__DEV__) {
    
    // 删除了 DEV 部分的代码
  } else {
    // 也就是Consumber对象指向React.Context对象    
    // 在<Consumer>进行渲染时,为了保证Consumer拿到最新的值,    
    // 直接让Consumer=React.Context,    
    // React.Context中的_currentValue已经被<Provider>的value给赋值了    
    // 所以Consumer能立即拿到最新的值
    context.Consumer = context;
  }
  
  // 删除了 DEV 部分的代码

  return context;
}

在 createContext 中,构建一个 context 对象,将传递进来的 defaultValue 赋值给 context 对象的 _currentValue 和 _currentValue2 属性,并在 context 对象上定义了一个用来追踪 context 并发渲染器数量的 _threadCount 属性,一个为 Consumer组件提供 value 的 Provider组件,和一个用于消费 context 的 Consumer组件。

_currentValue 和 _currentValue2 两个属性是为了适配不同的平台,如 Web端、移动端。这两个属性在 context 对象初始化时都会赋值为传入的 defaultValue 。在 React 更新的过程中,会一直有一个叫做 valueCursor 的栈,这个栈可以帮助记录当前的 context,每次更新组件的时候,_currentValue 和 _currentValue2 都会被赋值为最新的value(该部分内容暂不展开讲解,后续另开一篇文章详细讲解)。

context 对象构建好之后,就将当前的 context 对象分别挂载到 Provider 组件和 Consumer 组件上。

最后将该 context 对象返回,其数据结构如下:

4、使用注意事项

因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

为了防止这种情况,将 value 状态存储到父节点的 state 里:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state.value}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}