简单来讲, Context提供了一种直接访问祖先节点上的状态的方法, 避免了多级组件层层传递props.
有关Context的用法, 请直接查看官方文档, 本文将从fiber树构造的视角, 分析Context的实现原理.
根据官网示例, 通过React.createContext这个 api 来创建context对象. 在createContext中, 可以看到context对象的数据结构:
- export function createContext
( -
- defaultValue: T,
-
- calculateChangedBits: ?(a: T, b: T) => number,
-
- ): ReactContext
{ -
- if (calculateChangedBits === undefined) {
-
- calculateChangedBits = null;
-
- }
-
- const context: ReactContext
= { -
- $$typeof: REACT_CONTEXT_TYPE,
-
- _calculateChangedBits: calculateChangedBits,
-
- // 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,
-
- _threadCount: 0,
-
- Provider: (null: any),
-
- Consumer: (null: any),
-
- };
-
- context.Provider = {
-
- $$typeof: REACT_PROVIDER_TYPE,
-
- _context: context,
-
- };
-
- context.Consumer = context;
-
- return context;
-
- }
createContext核心逻辑:
context._currentValue(同时保存到context._currentValue2. 英文注释已经解释, 保存 2 个 value 是为了支持多个渲染器并发渲染)context.Provider, context.Consumer2 个reactElement对象.比如, 创建const MyContext = React.createContext(defaultValue);, 之后使用声明一个ContextProvider类型的组件.
在fiber树渲染时, 在beginWork中ContextProvider类型的节点对应的处理函数是updateContextProvider:
- function beginWork(
- current: Fiber | null,
- workInProgress: Fiber,
- renderLanes: Lanes,
- ): Fiber | null {
-
- const updateLanes = workInProgress.lanes;
-
- workInProgress.lanes = NoLanes;
-
- // ...省略无关代码
-
- switch (workInProgress.tag) {
-
- case ContextProvider:
-
- return updateContextProvider(current, workInProgress, renderLanes);
-
- case ContextConsumer:
-
- return updateContextConsumer(current, workInProgress, renderLanes);
-
- }
-
- }
-
- function updateContextProvider(
- current: Fiber | null,
- workInProgress: Fiber,
- renderLanes: Lanes,
- ) {
-
- // ...省略无关代码
-
- const providerType: ReactProviderType
= workInProgress.type; -
- const context: ReactContext
= providerType._context; -
- const newProps = workInProgress.pendingProps;
-
- const oldProps = workInProgress.memoizedProps;
-
- // 接收新value
-
- const newValue = newProps.value;
-
- // 更新 ContextProvider._currentValue
-
- pushProvider(workInProgress, newValue);
-
- if (oldProps !== null) {
-
- // ... 省略更新context的逻辑, 下文讨论
-
- }
-
- const newChildren = newProps.children;
-
- reconcileChildren(current, workInProgress, newChildren, renderLanes);
-
- return workInProgress.child;
-
- }
updateContextProvider()在fiber初次创建时十分简单, 仅仅就是保存了pendingProps.value做为context的最新值, 之后这个最新的值用于供给消费.
注意updateContextProvider -> pushProvider中的pushProvider(workInProgress, newValue):
- // ...省略无关代码
-
- export function pushProvider
(providerFiber: Fiber, nextValue: T): void { -
- const context: ReactContext
= providerFiber.type._context; -
- push(valueCursor, context._currentValue, providerFiber);
-
- context._currentValue = nextValue;
-
- }
pushProvider实际上是一个存储函数, 利用栈的特性, 先把context._currentValue压栈, 之后更新context._currentValue = nextValue.
与pushProvider对应的还有popProvider, 同样利用栈的特性, 把栈中的值弹出, 还原到context._currentValue中.
本节重点分析Context Api在fiber树构造过程中的作用. 有关pushProvider/popProvider的具体实现过程(栈存储), 在React 算法之栈操作中有详细图解.
使用了MyContext.Provider组件之后, 在fiber树构造过程中, context 的值会被ContextProvider类型的fiber节点所更新. 在后续的过程中, 如何读取context._currentValue?
在react中, 共提供了 3 种方式可以消费Context:
使用MyContext.Consumer组件: 用于JSX. 如,
beginWork中, 对于ContextConsumer类型的节点, 对应的处理函数是updateContextConsumer- function updateContextConsumer(
- current: Fiber | null,
- workInProgress: Fiber,
- renderLanes: Lanes,
- ) {
-
- let context: ReactContext
= workInProgress.type; -
- const newProps = workInProgress.pendingProps;
-
- const render = newProps.children;
-
- // 读取context
-
- prepareToReadContext(workInProgress, renderLanes);
-
- const newValue = readContext(context, newProps.unstable_observedBits);
-
- let newChildren;
-
- // ...省略无关代码
-
- }
使用useContext: 用于function中. 如, const value = useContext(MyContext)
class组件中, 使用一个静态属性contextType: 用于class组件中获取context. 如, MyClass.contextType = MyContext;
updateClassComponent后, 会调用prepareToReadContextcontext = readContext((contextType: any));所以这 3 种方式只是react根据不同使用场景封装的api, 内部都会调用prepareToReadContext和readContext(contextType).
- // ... 省略无关代码
-
- export function prepareToReadContext(
- workInProgress: Fiber,
- renderLanes: Lanes,
- ): void {
-
- // 1. 设置全局变量, 为readContext做准备
-
- currentlyRenderingFiber = workInProgress;
-
- lastContextDependency = null;
-
- lastContextWithAllBitsObserved = null;
-
- const dependencies = workInProgress.dependencies;
-
- if (dependencies !== null) {
-
- 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;
-
- }
-
- }
-
- }
-
- // ... 省略无关代码
-
- export function readContext
( -
- context: ReactContext
, -
- observedBits: void | number | boolean,
-
- ): T {
-
- const contextItem = {
-
- context: ((context: any): ReactContext
), -
- observedBits: resolvedObservedBits,
-
- next: null,
-
- };
-
- // 1. 构造一个contextItem, 加入到 workInProgress.dependencies链表之后
-
- if (lastContextDependency === null) {
-
- lastContextDependency = contextItem;
-
- currentlyRenderingFiber.dependencies = {
-
- lanes: NoLanes,
-
- firstContext: contextItem,
-
- responders: null,
-
- };
-
- } else {
-
- lastContextDependency = lastContextDependency.next = contextItem;
-
- }
-
- // 2. 返回 currentValue
-
- return isPrimaryRenderer ? context._currentValue : context._currentValue2;
-
- }
核心逻辑:
prepareToReadContext: 设置currentlyRenderingFiber = workInProgress, 并重置lastContextDependency等全局变量.readContext: 返回context._currentValue, 并构造一个contextItem添加到workInProgress.dependencies链表之后.注意: 这个readContext并不是纯函数, 它还有一些副作用, 会更改workInProgress.dependencies, 其中contextItem.context保存了当前context的引用. 这个dependencies属性会在更新时使用, 用于判定是否依赖了ContextProvider中的值.
返回context._currentValue之后, 之后继续进行fiber树构造直到全部完成即可.
来到更新阶段, 同样进入updateContextConsumer
- function updateContextProvider(
- current: Fiber | null,
- workInProgress: Fiber,
- renderLanes: Lanes,
- ) {
-
- const providerType: ReactProviderType
= workInProgress.type; -
- const context: ReactContext
= providerType._context; -
- const newProps = workInProgress.pendingProps;
-
- const oldProps = workInProgress.memoizedProps;
-
- const newValue = newProps.value;
-
- pushProvider(workInProgress, newValue);
-
- if (oldProps !== null) {
-
- // 更新阶段进入
-
- const oldValue = oldProps.value;
-
- // 对比 newValue 和 oldValue
-
- const changedBits = calculateChangedBits(context, newValue, oldValue);
-
- if (changedBits === 0) {
-
- // value没有变动, 进入 Bailout 逻辑
-
- if (
-
- oldProps.children === newProps.children &&
-
- !hasLegacyContextChanged()
-
- ) {
-
- return bailoutOnAlreadyFinishedWork(
-
- current,
-
- workInProgress,
-
- renderLanes,
-
- );
-
- }
-
- } else {
-
- // value变动, 查找对应的consumers, 并使其能够被更新
-
- propagateContextChange(workInProgress, context, changedBits, renderLanes);
-
- }
-
- }
-
- // ... 省略无关代码
-
- }
核心逻辑:
value没有改变, 直接进入Bailout(可以回顾fiber 树构造(对比更新)中对bailout的解释).value改变, 调用propagateContextChange- export function propagateContextChange(
- workInProgress: Fiber,
- context: ReactContext
, - changedBits: number,
- renderLanes: Lanes,
- ): void {
-
- 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;
-
- const list = fiber.dependencies;
-
- if (list !== null) {
-
- nextFiber = fiber.child;
-
- let dependency = list.firstContext;
-
- while (dependency !== null) {
-
- // 检查 dependency中依赖的context
-
- if (
-
- dependency.context === context &&
-
- (dependency.observedBits & changedBits) !== 0
-
- ) {
-
- // 符合条件, 安排调度
-
- if (fiber.tag === ClassComponent) {
-
- // class 组件需要创建一个update对象, 添加到updateQueue队列
-
- const update = createUpdate(
-
- NoTimestamp,
-
- pickArbitraryLane(renderLanes),
-
- );
-
- update.tag = ForceUpdate; // 注意ForceUpdate, 保证class组件一定执行render
-
- enqueueUpdate(fiber, update);
-
- }
-
- fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
-
- const alternate = fiber.alternate;
-
- if (alternate !== null) {
-
- alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
-
- }
-
- // 向上
-
- scheduleWorkOnParentPath(fiber.return, renderLanes);
-
- // 标记优先级
-
- list.lanes = mergeLanes(list.lanes, renderLanes);
-
- // 退出查找
-
- break;
-
- }
-
- dependency = dependency.next;
-
- }
-
- }
-
- // ...省略无关代码
-
- // ...省略无关代码
-
- fiber = nextFiber;
-
- }
-
- }
propagateContextChange源码比较长, 核心逻辑如下:
ContextProvider类型的节点开始, 向下查找所有fiber.dependencies依赖该context的节点(假设叫做consumer).consumer节点开始, 向上遍历, 修改父路径上所有节点的fiber.childLanes属性, 表明其子节点有改动, 子节点会进入更新逻辑.
- 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;
-
- }
-
- }
scheduleWorkOnParentPath与markUpdateLaneFromFiberToRoot的作用相似, 具体可以回顾fiber 树构造(对比更新)通过以上 2 个步骤, 保证了所有消费该context的子节点都会被重新构造, 进而保证了状态的一致性, 实现了context更新.
Context的实现思路还是比较清晰, 总体分为 2 步.
ContextConsumer节点调用readContext(MyContext)获取最新状态.ContextProvider节点负责查找所有ContextConsumer节点, 并设置消费节点的父路径上所有节点的fiber.childLanes, 保证消费节点可以得到更新.