Skip to content

第 8 章第 3 节:Consumer 的实现原理

在现代 React 中,消费(อ่าน)Context 主要有两种方式:useContext Hook 和 <Context.Consumer> 组件。useContext 因其简洁性而成为函数组件的首选,而 <Context.Consumer> 仍可用于类组件。

本节将重点分析 useContext 的内部实现,并简要介绍传统的 <Context.Consumer>

1. useContext Hook:简洁的背后

useContext Hook 的源码非常简洁,它遵循了所有 Hooks 的通用设计模式:将实际工作委托给当前的调度器(Dispatcher)。

javascript
// packages/react/src/ReactHooks.js

export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  // ... 省略开发环境下的警告
  return dispatcher.useContext(Context);
}

此代码揭示了两个关键点:

  1. resolveDispatcher():此函数根据组件的渲染阶段(首次挂载或更新)返回相应的 Hooks 调度器。
  2. dispatcher.useContext(Context):真正的逻辑位于调度器的 useContext 方法中。

这种关注点分离的设计使得 Hooks 的 API 保持稳定,同时内部实现可以根据需要进行优化和调整。

2. readContext:读取上下文的核心

无论是在挂载(mount)还是更新(update)阶段,dispatcher.useContext 最终都会调用 readContext 函数。这个函数是消费 Context 值的核心。

javascript
// packages/react-reconciler/src/ReactFiberHooks.js

function readContext<T>(
  context: ReactContext<T>,
): T {
  const value = isPrimaryRenderer ? context._currentValue : context._currentValue2;
  return value;
}

readContext 的实现出人意料地简单:它直接从 Context 对象的 _currentValue_currentValue2 属性中读取值。

  • _currentValue:这个属性在上一节中我们已经知道,它由 Provider 在渲染时动态更新。pushProvider 会在进入 Provider 子树前将新的 value 赋给它,而 popProvider 会在离开后恢复它。
  • 订阅更新useContext 不仅读取值,还必须确保当 Providervalue 发生变化时,消费该 Context 的组件能够重新渲染。这个“订阅”过程是在 readContext 中隐式完成的。当 readContext 被调用时,React 会将当前的组件(Fiber)与该 Context 关联起来。当 Provider 的值更新时,React 会遍历所有关联的组件,并调度它们进行更新。

3. 传统的 <Context.Consumer>

在 Hooks 出现之前,<Context.Consumer> 是消费 Context 的唯一方式。它的用法如下:

jsx
<MyContext.Consumer>
  {value => /* based on the value, render something */}
</MyContext.Consumer>

它使用了“渲染属性”(Render Props)模式,即组件的 children 是一个函数。<Context.Consumer> 组件在内部实现上与 useContext 类似,都会读取 Context 的当前值,并将其作为参数传递给 children 函数。

在 Fiber 架构下,<Context.Consumer> 对应的 Fiber 节点的 tagContextConsumer。在 beginWork 阶段,当处理到 ContextConsumer 类型的 Fiber 时,它会执行与 readContext 类似的操作来获取 Context 值,然后调用 children 函数并对其返回的 React 元素进行协调。

4. 总结

useContext Hook 和 <Context.Consumer> 组件为我们提供了在组件树中消费 Context 值的有效途径。

  • useContext 以其简洁的函数式风格成为现代 React 开发的首选。它的实现依赖于 Hooks 的调度器机制,最终通过 readContext 直接从 Context 对象上读取由 Provider 维护的当前值。
  • 订阅机制useContext 的关键。在读取值的过程中,React 会自动建立组件与 Context 之间的依赖关系,从而确保了在 Context 值变化时能够触发组件的自动更新。
  • <Context.Consumer> 作为传统的 API,虽然写法稍显繁琐,但其核心原理与 useContext 相同,都是为了读取 Context 值并触发必要的重新渲染。

通过对 ProviderConsumer 的深入分析,我们完整地理解了 React Context API 的工作流程:Provider 通过一个内部的“值栈”来维护和传递 value,而 Consumer(无论是 useContext 还是 <Context.Consumer>)则从这个体系中读取值并订阅更新。这个设计精妙地解决了跨层级状态传递的难题。

Last updated: