Skip to content

React.Context 源码详细解释

1. 概述

React.Context 是 React 提供的一种在组件树中共享数据的机制,无需显式地通过每一层组件传递 props。它主要用于解决多层组件间的数据传递问题,例如主题、用户登录状态等全局数据。

2. 使用

2.1. 旧版本上下文能力

在 v16.3.0 之前,React 使用的是 Class.contextTypes , Class.childContextTypes, Class.prototype.getChildContext 三个属性来实现上下文的能力,并借助于 PropTypes 来进行类型约束。具体如下:

2.1.1. 提供者

React 需要借助于 proptypes 包来申明 context 的类型,并进行类型约束。

  • Class.childContextTypes 用于声明子组件可以接受的 context 类型。
  • Class.prototype.getChildContext 用于返回当前组件的 context 数据。
js
import PropTypes from 'proptypes'

Class MyContextProvider extends React.Component {
  getChildContext() {
    return {
      key1: '张三',
    };
  }
  render() {
    return this.props.children;
  }
}
MyContextProvider.childContextTypes = {
  key1: PropTypes.string,
}

2.1.2. 消费者

  • Class.contextTypes 用于声明当前组件可以接受的 context 类型。
  • Class.prototype.context 用于获取当前组件的 context 数据。
js
import PropTypes from 'proptypes'
Class MyContextConsumer extends React.Component {
  render() {
    return <div>{this.context.key1}</div>;
  }
}
MyContextConsumer.contextTypes = {
  key1: PropTypes.string,
}

2.1.3. 解答

在 React16.3.0 之后,React 官方推荐使用 createContext 来创建上下文对象,然后通过 Provider 组件提供数据,Consumer 组件消费数据。 但是对于旧版的 Context 能力可以自己去开启,所以源码里面依然看到这三个属性, 并根据 disableLegacyContext 这一个编译属性决定是否禁用旧版本上下文的能力。

2.1.4. 提供者

js
// 1.  在 updateContainer 函数中,获取当前组件的上下文对象
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): Lane {
  const context = getContextForSubtree(parentComponent);
}

/**
 * 主要是获取当前组件的上下文 Context
 * @param {*} parentComponent
 * @returns
 */
function getContextForSubtree(
  parentComponent: ?React$Component<any, any>
): Object {
  if (!parentComponent) {
    return emptyContextObject;
  }
  // 获取父组件的 fiber 实例对象
  const fiber = getInstance(parentComponent);
  // 获取父组件的上下文对象
  const parentContext = findCurrentUnmaskedContext(fiber);

  if (fiber.tag === ClassComponent) {
    const Component = fiber.type;
    if (isLegacyContextProvider(Component)) {
      return processChildContext(fiber, Component, parentContext);
    }
  }

  return parentContext;
}

在 updateContainer 函数中,会调用 getContextForSubtree 函数来处理旧版本上下文能力中提供者数据的处理。 其一方面通过向上遍历最近的 类型是 ClassComponent 且 是一个旧版本提供者类型的组件,获取其 node.stateNode.__reactInternalMemoizedMergedChildContext 的值作为父级组件的上下文对象。

一方面通过 processChildContext(fiber, Component, parentContext)函数,通过 Component.prototype.getChildContext() 来获取当前组件的上下文对象的值,并对返回的上下文对象的数据按照 childContextTypes 进行过滤,最后根据上面父组件的 parentContext 来合并返回。

具体如下

js
/**
 * 处理组件的旧版上下文 context 提供者的逻辑, childContextTypes、getChildContext
 *   1. 获取 context 提供者的上下文对象
 *   2. 对 返回的上下文对象的数据按照 childContextTypes 进行过滤
 *   3. 合并 当前上下文context 和 父级上下文 parentContext
 * @param {*} fiber
 * @param {*} type
 * @param {*} parentContext
 * @returns
 */
function processChildContext(
  fiber: Fiber,
  type: any,
  parentContext: Object
): Object {
  const instance = fiber.stateNode;
  const childContextTypes = type.childContextTypes;

  // 1. 获取 context 提供者的上下文对象
  const childContext = instance.getChildContext();
  // 2. 对 返回的上下文对象的数据按照 childContextTypes 进行过滤
  for (const contextKey in childContext) {
    if (!(contextKey in childContextTypes)) {
      throw new Error(
        `${
          getComponentNameFromFiber(fiber) || "Unknown"
        }.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`
      );
    }
  }
  // 3. 合并 当前上下文context 和 父级上下文 parentContext
  return { ...parentContext, ...childContext };
}

2.1.5. 消费者

对于消费者,主要在处理组件的时候会使用旧版本 context 的能力,这时候就需要区分 ClassComponent 和 FunctionComponent 了。但是其实现逻辑是一样的,所以我们可以只看一下 ClassComponent 的处理逻辑。

js
// 1. 获取初始化的时候传入的 context 值
if (typeof contextType === "object" && contextType !== null) {
  // 读取 Class.contextType 传入的 context 对象
  context = readContext((contextType: any));
} else if (!disableLegacyContext) {
  // 获取未处理的 context 上下文的值
  unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
  // 是否定义了 contextTypes 属性
  const contextTypes = ctor.contextTypes;
  isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;
  // 如果定义了 contextTypes 属性, 则将 unmaskedContext 转换 按照 contextTypes 进行过滤
  //  否则直接返回空对象
  context = isLegacyContextConsumer
    ? getMaskedContext(workInProgress, unmaskedContext)
    : emptyContextObject;
}

这边优先判断 contextType 是否是对象,从而优先使用 新版本的上下文能力。如果不支持新版本 且 没有禁用旧版本的上下文能力,才会使用旧版本的上下文能力。

  1. 获取上下文中所有的 context 的数据
js
/**
 * 获取未过滤的 context 的值
 *   获取的是 contextStackCursor.current 的值
 * @param {*} workInProgress
 * @param {*} Component
 * @param {*} didPushOwnContextIfProvider
 * @returns
 */
function getUnmaskedContext(
  workInProgress: Fiber,
  Component: Function,
  didPushOwnContextIfProvider: boolean
): Object {
  // 禁用旧版 contextTypes 则返回空对象
  if (disableLegacyContext) {
    return emptyContextObject;
  } else {
    if (didPushOwnContextIfProvider && isContextProvider(Component)) {
      return previousContext;
    }
    // 返回 contextStackCursor.current 的值
    return contextStackCursor.current;
  }
}

这边直接返回的是 contextStackCursor.current 的值, 这个就涉及到 旧版本上下文数据的存储方式了。具体看下面

  1. 根据 context 的数据通过 contextTypes 进行过滤,返回最终的 context 对象。

在获取到未过滤的 context 的值之后,会根据 contextTypes 进行过滤,返回最终的 context 对象。

js
/**
 * 获取按照 contextTypes 过滤后的 context 对象
 *   如果没有定义 contextTypes 则返回空对象
 *   如果定义了 contextTypes 则按照 key 过滤 unmaskedContext 中的值
 *   并返回过滤后的 context 对象
 *   同时缓存到 react 的 instance.__reactInternalMemoizedUnmaskedChildContext 实例上
 * @param {*} workInProgress
 * @param {*} unmaskedContext
 * @returns
 */
function getMaskedContext(
  workInProgress: Fiber,
  unmaskedContext: Object
): Object {
  if (disableLegacyContext) {
    return emptyContextObject;
  } else {
    const type = workInProgress.type;
    // 获取组件上定义的 contextTypes  主要是 React16之前的方式
    const contextTypes = type.contextTypes;
    // 如果没有定义 contextTypes 则直接 { }
    if (!contextTypes) {
      return emptyContextObject;
    }
    const instance = workInProgress.stateNode;
    // 如果缓存了 且没有变化 直接使用缓存的 context
    if (
      instance &&
      instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
    ) {
      return instance.__reactInternalMemoizedMaskedChildContext;
    }
    // 按照 key 过滤 unmaskedContext 中的值
    const context = {};
    for (const key in contextTypes) {
      context[key] = unmaskedContext[key];
    }
    // 缓存到 react 实例上
    if (instance) {
      cacheContext(workInProgress, unmaskedContext, context);
    }

    return context;
  }
}

2.1.6. 旧版本上下文存储

在旧版本的上下文数据存储中其主要依赖以下几个属性 和 堆栈的数据结构存储 整个上下文数据的。

  1. 在 Fiber 节点上存储 提供者组件节点 当前的两个重要的上下文对象
  • node.stateNode.__reactInternalMemoizedUnmaskedChildContext 用于存储当前组件的未过滤的上下文对象
  • node.stateNode.__reactInternalMemoizedMaskedChildContext 用于存储当前组件的过滤后的上下文对象
  1. 通过堆栈的方式存储 FiberNode 数中的所有的 提供者组件节点 的上下文对象

2.2. 新版本上下文能力

React 通过createContext创建一个上下文对象,然后通过Provider组件提供数据,Consumer组件消费数据。

1. React.createContext 创建上下文对象

js
const MyContext = React.createContext(defaultValue);
  • defaultValue:可选的默认值,当没有 Provider 提供数据时使用。

2. Provider 提供数据

js
<MyContext.Provider value={"你要消费我"}>
  <App />
</MyContext.Provider>
  • value:要提供给消费组件的数据。

3.1. Consumer 消费数据

js
<MyContext.Consumer>
  {
    (value) => {
      <div>{value}</div>;
    } /* 基于 context 值进行渲染*/
  }
</MyContext.Consumer>
  • value:从最近的 Provider 中获取到的数据。

3.2. useContext 消费数据

js
function MyApp() {
  const value = useContext(MyContext);
  return <div>{value}</div>;
}

在 React 源码里,createContext 函数在创建新的 Context 对象时发挥着关键作用。它为开发者提供了一种便捷的方式来生成用于数据共享的 Context 实例。下面就为大家呈现简化后的源码示例:

3.3. Class 组件 contextType 消费数据

js
class MyApp extends React.Component {
  static contextType = MyContext;
  render() {
    return <div>{this.context}</div>;
  }
}

2.3. 重点

  1. 为什么要在 updateContainer 函数中获取当前组件的上下文对象?而不是像新版本在处理组件 Fiber 的时候获取?

  2. 新旧版本在 context 数据的存储 和 传输 上有啥区别?

3. 源码解析

3.1. createContext 函数的实现

主要是创建一个 Context 对象,这个对象包含了

  • Provider 和 Consumer 组件。
  • _currentValue 用于存储当前的 context 上下文的值。
  • _threadCount 用于跟踪当前 context 支持的并发渲染器数量。
js
export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    // 节点的类型为 REACT_CONTEXT_TYPE
    $$typeof: REACT_CONTEXT_TYPE,
    // 保存当前的 context 上下文的值
    // 当有两个渲染器时,会有两个值,一个是 primary,一个是 secondary
    _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,
  };
  // 提供一个消费者的对象
  context.Consumer = context;
  return context;
}

3.2. Provider 组件的实现

Provider 组件,他是一个生产者,主要是负责给当前内部的 Consumer 组件提供数据。

其主要也是在 beginWork 阶段,当组件的 type 为 REACT_PROVIDER_TYPE 时,会执行 updateContextProvider 函数。

updateContextProvider 函数

其主要解决以下几个问题

  1. FunctionComponent 节点中多个 useContext 的情况
js
function MyApp() {
  const value = useContext(MyContext);
  const value2 = useContext(MyContext2);
  return <div>{value}</div>;
}

对于这种 MyApp 组件来说,在 useContext 的时候会通过 readContext 函数来读取当前的 context 上下文的值,并将当前的 context 上下文的值添加到当前的 fiber 节点的 dependencies 链表中。

这样 MyApp 组件的 FiberNode 就变成了这样

js
{
  tag: FunctionComponent,
  type: MyApp,
  key: null,
  dependencies: {
    firstContext: {
      context: MyContext,
      next: {
        context: MyContext2,
        next: null
      }
    }
  }
}

其实现如下

js
export function readContext<T>(context: ReactContext<T>): T {
  // 构建一个 contextItem 对象
  // 并将其添加到当前的 fiber 节点的 dependencies 链表中
  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    memoizedValue: value,
    next: null,
  };
  // 使用全局变量 lastContextDependency 来记录当前的 context 上下文的值
  // 如果为 null ,则表明是第一次 useContext ,则将其赋值给 lastContextDependency 并将 dependencies 链表的 firstContext 指向当前的 contextItem
  // 如果不为 null ,则直接使用 lastContextDependency.next 来指向当前的 contextItem 即可
  if (lastContextDependency === null) {
    // This is the first dependency for this component. Create a new list.
    lastContextDependency = contextItem;
    currentlyRenderingFiber.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
    };
    if (enableLazyContextPropagation) {
      currentlyRenderingFiber.flags |= NeedsPropagation;
    }
  } else {
    // Append a new context item.
    lastContextDependency = lastContextDependency.next = contextItem;
  }
}

那么通过循环遍历 dependencies 链表,就可以找到函数式组件中订阅的所有的 context 上下文的值了。

  1. 相同 Provider 节点的嵌套 作用域问题

问题

js
function MyApp() {
  return (
    <MyConText.Provider value={1}>
      <MyConText.Provider value={2}>
        <MyConText.Consumer>
          {(value) => {
            return <div>{value}</div>; // 2
          }}
        </MyConText.Consumer>
      </MyConText.Provider>
      <MyConText.Consumer>
        {(value) => {
          return <div>{value}</div>; // 1
        }}
      </MyConText.Consumer>
    </MyConText.Provider>
  );
}

对于这种情况来说, 在内部的 Provider 节点中,其 Consumer 节点消费的应该是内部的 Provider 节点提供的值,而不是外部的 Provider 节点提供的值。那么如何解决的呐?

通过栈地方式去维护当前的 context 上下文的值。在 <MyConText.Provider>(内部)组件的时候通过 pushProvider(workInProgress, context, newValue); 将历史值 1 压入到栈中,并将 context._currentValue 的值修改成最新值 2 , 在 completeWork 阶段通过 popProvider(workInProgress, context); 将栈中的值 1 弹出,并将 context._currentValue 的值修改成 1。

这样就保证了内部作用域的隔离

  1. 订阅者 Consumer 节点的通知更新

通过深度优先的方式去遍历当前 Provider 节点的子孙节点,找到对应的订阅者节点,通过创建 update 对象 或者 标记 lane 的方式,来触发订阅者的更新

其主要方法是 propagateContextChange_eager 函数

propagateContextChange_eager 函数

源码

js
/**
 * 通过深度优先的方式去遍历当前 Provider节点的子孙节点,找到对应的订阅者节点。
 *    1. FunctionComponent 节点中多个 useContext的情况
 *    2. 相同Provider节点的嵌套 作用域问题
 * @param {*} workInProgress
 * @param {*} context
 * @param {*} renderLanes
 * @returns
 */
function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes
): void {
  // Only used by eager implementation
  if (enableLazyContextPropagation) {
    return;
  }
  let fiber = workInProgress.child;
  if (fiber !== null) {
    // Set the return pointer of the child to the work-in-progress fiber.
    fiber.return = workInProgress;
  }
  while (fiber !== null) {
    let nextFiber;

    // Visit this fiber.
    // 获取存在 dependencies 的 fiber 节点
    //  如 FunctionComponent 节点中使用了 useContext
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;
      // 遍历 dependencies 链表中是否存在当前的 context 即订阅者
      //  在函数式组件中 可能使用多个 useContext ,所以是一个链表,
      let dependency = list.firstContext;
      while (dependency !== null) {
        // Check if the context matches.
        // 如果找到当前的订阅者
        if (dependency.context === context) {
          // Match! Schedule an update on this fiber.
          // 对于 ClassComponent 节点来说, 生成一个 update 对象并添加到 updateQueue 中
          // 对于 FunctionComponent 节点来说, 生成一个 update 对象并添加到 updateQueue 中

          if (fiber.tag === ClassComponent) {
            // Schedule a force update on the work-in-progress.
            const lane = pickArbitraryLane(renderLanes);
            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 节点中存在当前的 lane
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 向上遍历的方式 将当前Fiber节点的 lane 都添加到 父节点的 childLanes 中
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress
          );

          // 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.
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      // Don't scan deeper if this is a matching provider
      // 如果遇到 相同的 子Provider 节点, 则停止遍历 应为下面的 Provider 节点会覆盖当前的 Provider 节点
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
      // If a dehydrated suspense boundary is in this subtree, we don't know
      // if it will have any context consumers in it. The best we can do is
      // mark it as having updates.
      const parentSuspense = fiber.return;

      if (parentSuspense === null) {
        throw new Error(
          "We just came from a parent so we must have had a parent. This is a bug in React."
        );
      }

      parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
      const alternate = parentSuspense.alternate;
      if (alternate !== null) {
        alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
      }
      // This is intentionally passing this fiber as the parent
      // because we want to schedule this fiber as having work
      // on its children. We'll use the childLanes on
      // this fiber to indicate that a context has changed.
      scheduleContextWorkOnParentPath(
        parentSuspense,
        renderLanes,
        workInProgress
      );
      nextFiber = fiber.sibling;
    } else {
      // Traverse down.
      nextFiber = fiber.child;
    }

    if (nextFiber !== null) {
      // Set the return pointer of the child to the work-in-progress fiber.
      // 将子节点的 return 指向当前节点
      nextFiber.return = fiber;
    } else {
      // No child. Traverse to next sibling.
      // 如果没有子节点了,遍历到下一个兄弟节点
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          // We're back to the root of this subtree. Exit.
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
          // Set the return pointer of the sibling to the work-in-progress fiber.
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        // No more siblings. Traverse up.
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

分析

  1. 对于 dependencies 链表中 context 的比较来判断是否是当前的订阅者

这个可以看上面的问题 1

  1. 通过 创建 update 对象 或者 标记 lane 的方式,来触发订阅者的更新

  2. 通过向上遍历的方式 将当前 Fiber 节点的 lane 都添加到 父节点的 childLanes 中

3.3. 订阅者的实现

对于 Context 的订阅者来说,其主要有以下三种方式来获取 context 上下文的值,且对于 React16 之前还提供了 contextTypesgetChildContext 来获取 context 上下文的值

3.3.1. Consumer 组件的实现

Consumer 组件就是在 createContext 函数中创建的 context.Consumer 对象。其本质就是 context 对象,只是 $$typeof === REACT_CONTEXT_TYPE

那么在 beginWork 阶段,当组件的 type 为 REACT_CONTEXT_TYPE 时,会执行 updateContextConsumer 函数。

updateContextConsumer 函数

核心流程

  1. prepareToReadContextreadContext 准备环境, 将 dependencies 的 firstContext 置为 null
  2. readContext 读取 context 中的最新的值
  3. 执行渲染函数 render(newValue),返回子节点
  4. 进入子节点的协调过程
js
/**
 * 处理 Context.Consumer 消费组件
 *   例子  : <MyContext.Consumer>{value => <div>{value}</div>}</MyContext.Consumer>
 * @param {*} current
 * @param {*} workInProgress
 * @param {*} renderLanes
 * @returns
 */
function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  let context: ReactContext<any> = workInProgress.type;

  const newProps = workInProgress.pendingProps;
  // 获取子节点
  const render = newProps.children;

  // 为 readContext 准备环境, 将 dependencies 的 firstContext 置为 null
  prepareToReadContext(workInProgress, renderLanes);
  // 读取 context 中的最新的值
  const newValue = readContext(context);
  if (enableSchedulingProfiler) {
    markComponentRenderStarted(workInProgress);
  }

  // 执行渲染函数,返回子节点
  newChildren = render(newValue);

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // 进入子节点的协调过程
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

prepareToReadContext 函数

作用: 为 readContext 准备环境, 将 dependencies 的 firstContext 置为 null

  • 对于 Consumer 组件

其订阅的 Provider 组件只可能是第一个 Provider 组件 ,所以其就是 prepareToReadContext() => readContext(context) => context

  • 对于 FunctionComponent 组件

其订阅的 Provider 组件可能是多个 Provider 组件,所以其就是 renderWithHooks 的时候 prepareToReadContext() => useContext()的时候 readContext(context) => `useContext()的时候 readContext(context)

源码

js
/**
 * 与 readContext 函数配合使用,用于在组件中处理上下文消费的准备工作
 *  1. 将组件的 dependencies 重置为 null
 *  2. 判断组件中是否存在当前优先级的更新,如存在强制更新
 * 使用场景:
 * - 函数组件开始渲染前( renderWithHooks )
 * - 类组件执行 render 方法前
 * - 上下文消费者组件更新时
 * - 并发渲染模式下的分片渲染阶段
 * @param {*} workInProgress
 * @param {*} renderLanes
 */
export function prepareToReadContext(
  workInProgress: Fiber,
  renderLanes: Lanes
): void {
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastFullyObservedContext = null;
  // 获取组件的 dependencies 链表
  const dependencies = workInProgress.dependencies;
  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.
          markWorkInProgressReceivedUpdate();
        }
        // Reset the work-in-progress list
        dependencies.firstContext = null;
      }
    }
  }
}

readContext 函数

作用: 生成一个 contextItem 对象,用于存储上下文的依赖关系, 并将其存储到 dependencies 链表中

源码

js
/**
 * 读取上下文的 context 中的值
 * @param {*} context
 * @returns
 */
export function readContext<T>(context: ReactContext<T>): T {
  // 获取 Context中当前的值
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    // 创建一个 contextItem 对象,用于存储上下文的依赖关系
    const contextItem = {
      // context 对象
      context: ((context: any): ReactContext<mixed>),
      // 当前的值
      memoizedValue: value,
      // 下一个 contextItem 对象
      next: null,
    };
    // 记录当前的 contextItem 对象
    // 如果 lastContextDependency 为 null,说明当前组件的 dependencies 链表为空
    //  则将当前的 contextItem 对象作为第一个节点
    // 如果 lastContextDependency 不为 null,说明当前组件的 dependencies 链表不为空
    //  则将当前的 contextItem 对象作为最后一个节点的 next 属性
    if (lastContextDependency === null) {
      if (currentlyRenderingFiber === null) {
        throw new Error();
      }

      // This is the first dependency for this component. Create a new list.
      lastContextDependency = contextItem;
      // 作为链表的第一个值
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        currentlyRenderingFiber.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

3.3.2. useContext 函数的实现

useContext 函数是 React 18 中新增的一个 Hook 函数,用于在函数组件中读取上下文的值。其用在 FunctionComponent 组件中。

主要流程

  1. updateFunctionComponent 函数中,会执行 prepareToReadContext 函数,将 dependencies 的 firstContext 置为 null
  2. useContext 函数中,就是执行 readContext 函数,生成一个 contextItem 对象,用于存储上下文的依赖关系, 并将其存储到 dependencies 链表中
js
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useContext: readContext,
};

3.3.3. Class 组件 contextType 的实现

js
function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes
) {
  // Push context providers early to prevent context stack mismatches.
  // During mounting we don't know the child context yet as the instance doesn't exist.
  // We will invalidate the child context in finishClassComponent() right after rendering.
  let hasContext;
  if (isLegacyContextProvider(Component)) {
    hasContext = true;
    pushLegacyContextProvider(workInProgress);
  } else {
    hasContext = false;
  }
  //
  prepareToReadContext(workInProgress, renderLanes);
  // 获取 class 的实例对象
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  // 首次渲染
  if (instance === null) {
    // In the initial pass we might need to construct the instance.
    constructClassInstance(workInProgress, Component, nextProps);
  }
  return nextUnitOfWork;
}

function constructClassInstance(
  workInProgress: Fiber,
  ctor: any,
  props: any
): any {
  let isLegacyContextConsumer = false;
  let unmaskedContext = emptyContextObject;
  let context = emptyContextObject;
  // 获取 class 对象上的 contextType 属性
  const contextType = ctor.contextType;

  // 1. 获取初始化的时候传入的 context 值
  //
  if (typeof contextType === "object" && contextType !== null) {
    context = readContext((contextType: any));
  } else if (!disableLegacyContext) {
    unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
    const contextTypes = ctor.contextTypes;
    isLegacyContextConsumer =
      contextTypes !== null && contextTypes !== undefined;
    context = isLegacyContextConsumer
      ? getMaskedContext(workInProgress, unmaskedContext)
      : emptyContextObject;
  }

  return instance;
}

3.3.4 Class 组件 contextTypes 的实现

这是 React16 之前定义 context 上下文的方式

其没有通过 Class.contextType 去指定获取哪一个上下文的内容,而是直接通过 contextStackCursor.current 去访问 context 栈中最新的值的内容, 然后通过 Class.contextTypes 去过滤只获取哪些属性的内容。所以其对于上下文 context 的值是通过组件所在位置来决定,而不是由组件本身来决定

对于这种方式,在后面通过 Class.contextType 去显示的指定获取哪一个上下文的内容。

源码

新旧版上下文判断

js
/**
 * 是否支持且存在旧版上下文逻辑
 *   childContextTypes 是否存在
 * @param {*} type
 * @returns
 */
function isContextProvider(type: Function): boolean {
  if (disableLegacyContext) {
    return false;
  } else {
    const childContextTypes = type.childContextTypes;
    return childContextTypes !== null && childContextTypes !== undefined;
  }
}

通过判断当前版本是否禁用旧版上下文逻辑,和 Class 组件本身是否定义了 childContextTypes 属性来判断是否支持旧版上下文逻辑。

上下文值的获取

js
// 1. 获取初始化的时候传入的 context 值
if (typeof contextType === "object" && contextType !== null) {
  // 读取 Class.contextType 传入的 context 对象
  context = readContext((contextType: any));
} else if (!disableLegacyContext) {
  // 获取未处理的 context 上下文的值
  unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
  // 是否定义了 contextTypes 属性
  const contextTypes = ctor.contextTypes;
  isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;
  // 如果定义了 contextTypes 属性, 则将 unmaskedContext 转换 按照 contextTypes 进行过滤
  //  否则直接返回空对象
  context = isLegacyContextConsumer
    ? getMaskedContext(workInProgress, unmaskedContext)
    : emptyContextObject;
}
  1. 优先根据 Class.contextType 传入的 context 对象来获取 context 值, 如果定义了则直接不对旧版上下文逻辑进行处理
  2. 如果没有定义 Class.contextType
  • getUnmaskedContext(workInProgress, ctor, true) 获取当前 context 栈中最新的上下文内容
  • 根据是否定义 Class.contextTypes 来判断是否需要过滤上下文内容,即 getMaskedContext(workInProgress, unmaskedContext)
getUnmaskedContext 函数

对于旧版上下文逻辑其主要通过直接获取 contextStackCursor.current 的值来获取 context 上下文对象。这个值可以在 pushProvider 方法中看到

js
/**
 * 获取未过滤的 context 的值
 *   获取的是 contextStackCursor.current 的值
 * @param {*} workInProgress
 * @param {*} Component
 * @param {*} didPushOwnContextIfProvider
 * @returns
 */
function getUnmaskedContext(
  workInProgress: Fiber,
  Component: Function,
  didPushOwnContextIfProvider: boolean
): Object {
  // 禁用旧版 contextTypes 则返回空对象
  if (disableLegacyContext) {
    return emptyContextObject;
  } else {
    if (didPushOwnContextIfProvider && isContextProvider(Component)) {
      // If the fiber is a context provider itself, when we read its context
      // we may have already pushed its own child context on the stack. A context
      // provider should not "see" its own child context. Therefore we read the
      // previous (parent) context instead for a context provider.
      return previousContext;
    }
    // 返回 contextStackCursor.current 的值
    return contextStackCursor.current;
  }
}
getMaskedContext 函数

按照 contextTypes 的值对 unmaskedContext 进行过滤

js
/**
 * 获取按照 contextTypes 过滤后的 context 对象
 *   如果没有定义 contextTypes 则返回空对象
 *   如果定义了 contextTypes 则按照 key 过滤 unmaskedContext 中的值
 *   并返回过滤后的 context 对象
 *   同时缓存到 react 的 instance.__reactInternalMemoizedUnmaskedChildContext 实例上
 * @param {*} workInProgress
 * @param {*} unmaskedContext
 * @returns
 */
function getMaskedContext(
  workInProgress: Fiber,
  unmaskedContext: Object
): Object {
  if (disableLegacyContext) {
    return emptyContextObject;
  } else {
    const type = workInProgress.type;
    // 获取组件上定义的 contextTypes  主要是 React16之前的方式
    const contextTypes = type.contextTypes;
    // 如果没有定义 contextTypes 则直接 { }
    if (!contextTypes) {
      return emptyContextObject;
    }

    // Avoid recreating masked context unless unmasked context has changed.
    // Failing to do this will result in unnecessary calls to componentWillReceiveProps.
    // This may trigger infinite loops if componentWillReceiveProps calls setState.
    const instance = workInProgress.stateNode;
    // 如果缓存了 且没有变化 直接使用缓存的 context
    if (
      instance &&
      instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
    ) {
      return instance.__reactInternalMemoizedMaskedChildContext;
    }
    // 按照 key 过滤 unmaskedContext 中的值
    const context = {};
    for (const key in contextTypes) {
      context[key] = unmaskedContext[key];
    }

    // 缓存到 react 实例上
    if (instance) {
      cacheContext(workInProgress, unmaskedContext, context);
    }

    return context;
  }
}