阅读 245

React 源码解读之 React Context

React 的 Context 属性实现了 props 在层级组件间跨层级传递,从而避免了props逐层传递的繁琐。

Context 的用法,请阅读官方文档,本文将从源码层面,从 Context 的创建、消费以及更新三个方面来介绍 Context。

1、Context 的创建

const MyContext = React.createContext(defaultValue);复制代码

官方提供了 React.createContext 这个 API 来创建 context 对象,我们来看看这个 API 是如何创建 context 对象的。

1.1 类型定义

通过 React.createContext 创建的 context 对象的类型定义如下:

// packages/shared/ReactTypes.js
export type ReactContext<T> = {
  // ReactContext中的$$typeof是作为createElement中的属性type中的对象进行存储的
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>, // 消费 context 的组件
  Provider: ReactProviderType<T>, // 提供 context 的组件
  // 保存 2 个 value 是为了支持多个渲染器并发渲染
  _currentValue: T,  // Provider组件 的 value 属性
  _currentValue2: T, // Provider组件 的 value 属性
  _threadCount: number, // 用来追踪 context 的并发渲染器数量
  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
  // This value may be added by application code
  // to improve DEV tooling display names
  displayName?: string,
  ...
};复制代码

1.2 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 组件提供 context 的 Provider 组件,和一个用于消费 context 的 Consumer 组件。

_currentValue 和 _currentValue2 两个属性是为了支持多个渲染器并发渲染。这两个属性在 context 对象初始化时都会赋值为传入的 defaultValue 。在 React 更新的过程中,会一直有一个叫做 valueCursor 的栈,这个栈可以帮助记录当前的 context,每次更新组件的时候,_currentValue 和 _currentValue2 都会被赋值为最新的value 。

在 Provider 组件和 Consumer 组件上,也同时挂载了 context 对象,因此我们最终看到到 context 对象如下:

1.3 context._currentValue 的存储

Fiber 树渲染时会从 beginWork() 函数开始执行,在 beginWork 中 ContextProvider 类型的节点会调用 updateContextProvider 来处理 context:

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 删除了无关代码
  switch (workInProgress.tag) {    
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}




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;

  // 新的 value,(Context.Provider 组件的 value)
  const newValue = newProps.value;

  // 删除了 Dev 部分的代码

  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;

      // 这里也就是我们写在 Provider 上的 value 要存在父组件的 state 中的原因
      // 如果是个直接定义到 value 上的对象,由于每次比较都不同(浅比较对象的地址),
      // 每次都会触发所有 Consumer 组件重新更新,引起不必要的性能消耗

      // 浅比较 旧的value 和新的value
      if (is(oldValue, newValue)) {
        // No change. Bailout early if children are the same.
        // value 没有改变,如果 children 也没有改变的话,进入 Bailout 逻辑
        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.
        // context value 改变,则去寻找匹配的 consumers 组件去更新
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }

  // 涉及到 fiber 的协调,暂不看
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}复制代码

在 fiber 初次创建时,会读取 fiber 节点上的 pendingProps 属性,然后将存储在 pendingProps 上的 value(Provider 组件的value属性) 作为context._currentValue的最新值,之后这个最新的值供给 Consumer 消费,并同时将这个最新值存入栈中。

context._currentValue的存储是在 pushProvider 函数中完成的,我们来看看这个函数。

1.4 pushProvider

// packages/react-reconciler/src/ReactFiberNewContext.new.js
export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T,
): void {
  if (isPrimaryRenderer) {
    // 将 _currentValue 压入栈中,valueCursor 是一个只有 current 属性的对象,用来记录当前的 context._currentValue
    push(valueCursor, context._currentValue, providerFiber);
    // 将 _currentValue 更新为新的 value
    context._currentValue = nextValue;
    
    // 删除了 Dev部分的代码
  } else {
    // 将 _currentValue2 压入栈中,valueCursor 是一个只有 current 属性的对象,用来记录当前的 context._currentValue2
    push(valueCursor, context._currentValue2, providerFiber);
    // 将 _currentValue2 更新为新的 value
    context._currentValue2 = nextValue;
    
    // 删除了 Dev部分的代码
    
  }
}复制代码

pushProvider 做的事情很简单,就是将 _currentValue 和 _currentValue2 存储起来,并将 context对象上的 _currentValue 和 _currentValue2 更新为最新的 value,供给 Consumer组件 消费。

1.5 push

在存储 _currentValue 和 _currentValue2 的时候,使用到了栈这种数据结构:

// packages/react-reconciler/src/ReactFiberStack.new.js

// 定义一个 valueStack 的栈,存储 context 的value
const valueStack: Array<any> = [];

function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
  index++;

  // 入栈
  valueStack[index] = cursor.current;

  if (__DEV__) {
    fiberStack[index] = fiber;
  }
	
  // 更新最新的 context._currentValue
  cursor.current = value;
}复制代码

push 函数的第一个参数 cursor 是一个只有 current 属性的对象,用来存储最新的 context._currentValue,在 packages/react-reconciler/src/ReactFiberNewContext.new.js 文件中创建了一个全局的 valueCursor 对象,用来存储最新的 context._currentValue:

// packages/react-reconciler/src/ReactFiberNewContext.new.js
const valueCursor: StackCursor<mixed> = createCursor(null);复制代码

createCursor 创建函数如下:

// packages/react-reconciler/src/ReactFiberStack.new.js
function createCursor<T>(defaultValue: T): StackCursor<T> {
  return {
    current: defaultValue,
  };
}复制代码

小结

Context 对象通过 React.createContext 这个API来创建,在 createContext 中,创建了一个 ReactContext 类型的 context 对象,并把该对象分别添加到了context 对象自身的 Provider 和 Consumer 组件上,即 Provider 和 Consumer 的_context 属性指向的是 context 对象自己。

创建好 context 对象后,通过调用 pushProvider 方法将 Provider 组件的 value 属性值保存到栈中,并将最新的 value 更新到 context 对象上,以供 Consumer 组件、class组件的 contextType静态属性以及 Hook函数useContext 消费。

2、Context 的消费

使用了 Context.Provider 组件提供value之后,每次更新组件的时候,ContextProvider 类型的 fiber 节点都会去执行 pushProvider 函数,把 context._currentValue 和 context._currentValue2 指向最新的 context value(Provider组件的value属性),那么在后续的过程中,是如何读取 context._currentValue 和context._currentValue2 并消费它的呢?

在 React 中,有三种方式可以消费 context:

  • 使用 Context.Consumer 组件

  • 使用 useContext

  • 使用 class 组件的 contextType 静态属性

下面,我们分别来介绍这三种方式。

2.1 Context.Consumer消费context

使用 Context.Consumer 组件,在 JSX 中消费 context:

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>复制代码

这种方法需要一个函数作为子元素(function as a child)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

那么,对于 Context.Consumer 这种方式,源码是如何读取 context 的呢?

在 beginWork 函数中,对于 ContextConsumer 类型的节点,context 的读取发生在 updateContextConsumer 函数中。

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 删除了无关代码
  switch (workInProgress.tag) {    
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
  }
}





function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  let context: ReactContext<any> = workInProgress.type;
  
  // 删除了 Dev 部分的代码
  
  const newProps = workInProgress.pendingProps;
  const render = newProps.children;

  // 删除了 Dev 部分的代码
  
  // 读取 context

  // 重置 context 依赖列表,并将当前 fiber 节点标记为需要执行更新
  prepareToReadContext(workInProgress, renderLanes);
  // 从 context 上读取Provider 组件提供的 value,供 Consumer 消费
  const newValue = readContext(context);
  if (enableSchedulingProfiler) {
    // 给组件加上开始渲染的标记
    markComponentRenderStarted(workInProgress);
  }
  let newChildren;
  if (__DEV__) {
    
    // 删除了 Dev 部分的代码
    
  } else {
    newChildren = render(newValue);
  }
  if (enableSchedulingProfiler) {
    markComponentRenderStopped();
  }

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}复制代码

首先从当前工作的fiber 节点上读取 context:

 let context: ReactContext<any> = workInProgress.type;复制代码

然后调用 readContext 获取 context 对象上的 _currentValue:

const newValue = readContext(context);复制代码

接着将获取到的 _currentValue 交给 render 消费:

newChildren = render(newValue);复制代码

2.2 useContext 消费 context

useContext 是一个 Hook 函数,用于 Function组件中。它接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

对于 Function 类型的组件,在 beginWork 函数中会调用 updateFunctionComponent 函数来处理当前工作的 fiber 节点:

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 删除了无关代码
  switch (workInProgress.tag) {    
     case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  }
}复制代码

然后在 updateFunctionComponent 中调用 prepareToReadContext 重置 context 依赖列表,并将当前 fiber 节点标记为需要执行更新,最后将 context 交给 renderWithHooks

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  
    // 删除了Dev部分的daunt

  let context;
  if (!disableLegacyContext) {
     // 读取 context
    const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    context = getMaskedContext(workInProgress, unmaskedContext);
  }

  let nextChildren;
  // 重置 context 依赖列表,并将当前 fiber 节点标记为需要执行更新
  prepareToReadContext(workInProgress, renderLanes);
  if (enableSchedulingProfiler) {
    markComponentRenderStarted(workInProgress);
  }
  if (__DEV__) {
   // 删除了Dev部分的daunt
  } else {
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
  }
  // 删除了无关代码
}复制代码

在 renderWithHooks 中并没有看到对 context 的处理,renderWithHooks主要是对 ReactCurrentDispatcher 做了赋值,而 ReactCurrentDispatcher 上挂载的是 hook 函数(详情移步:React Hooks 源码解读之 Hook 入口):

我们知道,调用 useContext,实际上调用的是 readContext (详情阅读:React Hooks 源码解读之 Hook 入口),readContext 将 context 中的 _currentValue 读取出来,供给组件使用。reactContext 将在下文介绍。

2.3 contextType 消费 context

在 class 组件中,使用一个静态属性 contextType 来消费 context 。

挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。

如下例子:

const MyContext = React.createContext(defaultValue);

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;复制代码

对于 Class 类型的组件,在 beginWork 函数中会调用 updateClassComponent 函数来处理当前工作的 fiber 节点:

// packages/react-reconciler/src/ReactFiberBeginWork.new.js

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 删除了无关代码
  switch (workInProgress.tag) {
    // Class 类型的组件
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      
      // 调用 updateClassComponent 处理当前fiber节点
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  }
}复制代码

然后在 updateClassComponent 中调用 prepareToReadContext 重置 context 依赖列表,并将当前 fiber 节点标记为需要执行更新,最后将 context 交给 renderWithHooks

// packages/react-reconciler/src/ReactFiberBeginWork.new.js
function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  
  // 调用 prepareToReadContext 重置 context 依赖列表,
  // 并将当前 fiber 节点标记为需要执行更新,
  // 最后将 context 交给 renderWithHooks 
  prepareToReadContext(workInProgress, renderLanes);

  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    
    // constructClassInstance 内部会调用 context = readContext((contextType: any)); 来处理context
    constructClassInstance(workInProgress, Component, nextProps);
    
     // mountClassInstance 内部会调用 instance.context = readContext(contextType); 来处理context
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {

    // resumeMountClassInstance 内部会调用 nextContext = readContext(contextType); 来处理 context
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    // updateClassInstance 内部会调用 nextContext = readContext(contextType); 来处理 context
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
 
  return nextUnitOfWork;
}复制代码

在 updateClassComponent 中,无论是 constructClassInstance()、mountClassInstance()、resumeMountClassInstance()、还是 updateClassInstance(),都会调用 readContext() 读取 context。

以上三种消费 context 的方式首先都会调用 prepareToReadContext 重置 context 依赖列表,并将当前 fiber 节点标记为需要执行更新,然后再调用 readContext 读取 context 。我们先来看看 prepareToReadContext。

2.4 prepareToReadContext

// packages/react-reconciler/src/ReactFiberNewContext.new.js
export function prepareToReadContext(
  workInProgress: Fiber,
  renderLanes: Lanes,
): void {
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastFullyObservedContext = null;

  // 在 readContext() 里创建了 context 依赖列表,并挂载到了 currentlyRenderingFiber 节点上
  // 在此处将 currentlyRenderingFiber 节点上的依赖列表读取出来
  const dependencies = workInProgress.dependencies;
  // 重置 依赖列表上的 context
  if (dependencies !== null) {
    if (enableLazyContextPropagation) {
      // Reset the work-in-progress list
      dependencies.firstContext = null;
    } else {
      const firstContext = dependencies.firstContext;
      if (firstContext !== null) {
        if (includesSomeLane(dependencies.lanes, renderLanes)) {
          // Context list has a pending update. Mark that this fiber performed work.
          // context 列表上有待更新的update,将当前 fiber 标记为需要执行更新
          markWorkInProgressReceivedUpdate();
        }
        // Reset the work-in-progress list
        dependencies.firstContext = null;
      }
    }
  }
}复制代码

可以看到,prepareToReadContext 只是把 readContext 里创建的 context 依赖列表从 currentlyRenderingFiber 节点上读取了出来。我们现在来看看 readContext 。

2.5 readContext

// packages/react-reconciler/src/ReactFiberNewContext.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) {
      invariant(
        currentlyRenderingFiber !== null,
        'Context can only be read while React is rendering. ' +
        'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
        'In function components, you can read it directly in the function body, but not ' +
        'inside Hooks like useReducer() or useMemo().',
      );

      // 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 返回出去。

小结

Context 的消费有三种方式,分别是 Context.Consumer 组件、Hook函数useContext 以及 class 组件的contextType 静态属性。这三种方式是 react 根据不同的使用场景封装的 api ,它们内部都会调用 prepareToReadContext 把在 readContext 里创建的 context 依赖列表从 currentlyRenderingFiber 节点上读取出来,以供消费。然后会继续调用 readContext 来处理 context 依赖列表。

3、Context 的更新

React 在更新的时候,对于 Context ,会进入 updateContextProvider:

// packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  switch (workInProgress.tag) {
    case ContextProvider:
			return updateContextProvider(current, workInProgress, renderLanes);
  }
}  
复制代码

我们来看看 updateContextProvider。

3.1 updateContextProvider

// packages/react-reconciler/src/ReactFiberBeginWork.new.js
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;

  // 删除了 Dev部分的代码

  // 将 _currentValue 和 _currentValue2 存储起来,并将 context 上的 _currentValue/_currentValue2 更新为最新的 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) {

      // 这里也就是我们写在 Provider 组将上的 value 要存在一个父组件的state中的原因
      // 如果是个直接定义到 value 上的对象,则每次比较都不同,
      // 每次都会触发所有 consumer 组件,引起不必要的性能消耗

      const oldValue = oldProps.value;
      // 比较 newValue 和 oldValue,is方法是浅比较
      if (is(oldValue, newValue)) {
        
        // value 没有改变,如果 children 也没有发生改变的话,进入 bailout 逻辑
        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.
        // context value 发生变化,去寻找匹配的 consumers 组件去更新
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}复制代码

在 React 更新的过程中,会一直有一个 valueCursor 的对象,这个对象用来记录当前的context,每次更新组件的时候,都会去执行 pushProvider函数,将 _currentValue 和 _currentValue2 存入栈中,并将 context 上的 _currentValue 和 _currentValue2 更新为最新的 value。

pushProvider(workInProgress, context, newValue);复制代码

pushProvider 在上文已有介绍,此处不再赘述。

将 context 的 value 入栈后,比较context的 newValue 和 oldValue:

  • 如果 value 没有变化,并且 children 也没有发生改变的话,进入 bailout 逻辑。

  • 如果 value 发生了变化,则调用 propagateContextChange,寻找匹配的 consumers 组件去更新。

    const oldValue = oldProps.value; // 比较 newValue 和 oldValue,is方法是浅比较 if (is(oldValue, newValue)) {

    // value 没有改变,如果 children 也没有发生改变的话,进入 bailout 逻辑
    if (
      oldProps.children === newProps.children &&
      !hasLegacyContextChanged()
    ) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    }复制代码

    } else { // context value 发生变化,去寻找匹配的 consumers 组件去更新 propagateContextChange(workInProgress, context, renderLanes); }

在比较 newValue 和 oldValue 时,调用的是 is 方法,它是 React 自定义的方法,值得注意的是 is 比较是浅比较。

// packages/shared/objectIs.js
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}复制代码

这也就是我们写在 Provider 组件上的 value 要存在一个父组件的state中的原因,如果是个直接定义到 value 上的对象,则每次比较都不同,每次都会触发所有 consumer 组件渲染,引起不必要的性能消耗。

在比较 context 的 newValue 和 oldValue 时,如果 value 发生了变化,会调用 propagateContextChange 去寻找匹配的 consumers 组件去更新,下面,我们来看看 propagateContextChange的实现。

3.2 propagateContextChange

// packages/react-reconciler/src/ReactFiberNewContext.new.js
export function propagateContextChange<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
  if (enableLazyContextPropagation) {
    // TODO: This path is only used by Cache components. Update
    // lazilyPropagateParentContextChanges to look for Cache components so they
    // can take advantage of lazy propagation.

    // 寻找匹配的 CacheComponent
    const forcePropagateEntireTree = true;
    propagateContextChanges(
      workInProgress,
      [context],
      renderLanes,
      forcePropagateEntireTree,
    );
  } else {
    // 查找匹配的 context,标记更新的优先级
    propagateContextChange_eager(workInProgress, context, renderLanes);
  }
}复制代码

可以看到,在 propagateContextChange 中,对于 CacheComponent,调用 propagateContextChanges 来寻找匹配的consumers,而其它类型的组件,则调用 propagateContextChange_eager 来寻找匹配的 consumers。

propagateContextChanges 和 propagateContextChange_eager 的功能差不多,都是遍历fiber链表,然后遍历fiber节点上的 context 依赖列表( context 依赖列表是一个链表结构),寻找匹配的 consumers,然后将当前fiber节点的update优先级标记为高优先级,并修改当前fiber节点父路径上所有节点的 childLanes 属性。

下面我们来重点看下 propagateContextChange_eager 的实现。

3.3 propagateContextChange_eager

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
  // Only used by eager implemenation
  if (enableLazyContextPropagation) {
    return;
  }
  let fiber = workInProgress.child;
  if (fiber !== null) {
    // 将 fiber 的 return 属性指向当前工作的 workInProgress
    // fiber 节点的 return 属性指向父节点
    fiber.return = workInProgress;
  }
  while (fiber !== null) {
    let nextFiber;

    // 在 readContext() 中创建了 context 的依赖列表,并将依赖列表添加到了 fiber节点上
    // 这里从 fiber 节点上取出 context 的依赖列表,对依赖列表进行检查
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // Check if the context matches.
        // 查找匹配的 consumers
        if (dependency.context === context) {
          // Match! Schedule an update on this fiber.
          // 找到匹配的 context,则安排调度
          if (fiber.tag === ClassComponent) {
            // Schedule a force update on the work-in-progress.
            // 设置 update 为 高优先级
            const lane = pickArbitraryLane(renderLanes);
            // 将当前 fiber 设置为 ForceUpdate,保证 class 组件一定执行 render
            const update = createUpdate(NoTimestamp, lane);
            update.tag = ForceUpdate;
            // TODO: Because we don't have a work-in-progress, this will add the
            // update to the current fiber, too, which means it will persist even if
            // this render is thrown away. Since it's a race condition, not sure it's
            // worth fixing.

            // Inlined `enqueueUpdate` to remove interleaved update check
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
              // Only occurs if the fiber has been unmounted.
            } else {
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
                // This is the first update. Create a circular list.
                update.next = update;
              } else {
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }

          // 标记优先级
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 修改当前fiber节点父路径上所有节点的 childLanes 属性
          scheduleWorkOnParentPath(fiber.return, renderLanes);

          // Mark the updated lanes on the list, too.
          // 标记优先级
          list.lanes = mergeLanes(list.lanes, renderLanes);

          // Since we already found a match, we can stop traversing the
          // dependency list.
          // 已经找到了匹配的 context,退出遍历依赖列表
          break;
        }
        dependency = dependency.next;
      }
    } 
    
    // 删除了非主要逻辑的代码


    fiber = nextFiber;
  }
}复制代码

propagateContextChange_eager 的核心处理逻辑主要有两点:

  1. 寻找匹配的 consumers:从 ContextProvider 类型的节点开始,遍历 fiber 链表,从fiber上的context依赖列表寻找匹配的 consumers,然后将当前fiber节点的update优先级标记为高优先级。

  2. 修改父路径上所有节点的childLanes属性:寻找到了匹配的 consumers 之后,调用scheduleWorkOnParentPath,修改当前fiber节点父路径上所有节点的 childLanes 属性,表明其子节点有改动,子节点会进入更新逻辑。scheduleWorkOnParentPath函数源码如下。

3.4 scheduleWorkOnParentPath

// packages/react-reconciler/src/ReactFiberNewContext.new.js
export function scheduleWorkOnParentPath(
  parent: Fiber | null,
  renderLanes: Lanes,
) {
  // Update the child lanes of all the ancestors, including the alternates.
  let node = parent;
  while (node !== null) {
    const alternate = node.alternate;
    if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
      node.childLanes = mergeLanes(node.childLanes, renderLanes);
      if (alternate !== null) {
        alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
      }
    } else if (
      alternate !== null &&
      !isSubsetOfLanes(alternate.childLanes, renderLanes)
    ) {
      alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
    } else {
      // Neither alternate was updated, which means the rest of the
      // ancestor path already has sufficient priority.
      break;
    }
    node = node.return;
  }
}复制代码

通过以上两个步骤,保证了消费该 context 的所有子节点都会被重新渲染,进而保证了状态的一致性,实现了 context 更新。

4、流程图

5、总结

  1. Context 对象由 React.createContext() API 创建,新创建的 context 对象会被挂载到自身的 Provider 和 Consumer 组件上,pushProvider() 函数会把 Provider 组件的 value 属性值保存到栈中,并将最新的 value 更新到 context 对象上。

  2. context 的消费有三种方式,它们是:Context.Consumer组件、Hook函数useContext 和 class组件的 contextType静态属性,它们内部都会调用 prepareToReadContext 把在 readContext 里创建的 context 依赖列表从 currentlyRenderingFiber 节点上读取出来,以供消费。然后会继续调用 readContext 来处理 context 依赖列表。

  3. 在更新的时候,由 ContextProvider 节点负责查找所有消费了当前 context 的 ContextConsumer 节点,将其update的优先级设置为高优先级,并设置消费节点父路径上所有fiber节点的 childLanes,保证所有消费了该 context 的节点都可以得到更新。


作者:紫圣
链接:https://juejin.cn/post/7030934425760169991


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐