title: context 原理
React Context 原理
简单来讲, Context提供了一种直接访问祖先节点上的状态的方法, 避免了多级组件层层传递props.
有关Context的用法, 请直接查看官方文档, 本文将从fiber树构造的视角, 分析Context的实现原理.
创建 Context
根据官网示例, 通过React.createContext这个 api 来创建context对象. 在createContext中, 可以看到context对象的数据结构:
export function createContext<T>(defaultValue: T,calculateChangedBits: ?(a: T, b: T) => number,): ReactContext<T> {if (calculateChangedBits === undefined) {calculateChangedBits = null;}const context: ReactContext<T> = {$$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);, 之后使用<MyContext.Provider value={/* 某个值 */}>声明一个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<any> = workInProgress.type;const context: ReactContext<any> = providerType._context;const newProps = workInProgress.pendingProps;const oldProps = workInProgress.memoizedProps;// 接收新valueconst newValue = newProps.value;// 更新 ContextProvider._currentValuepushProvider(workInProgress, newValue);if (oldProps !== null) {// ... 省略更新context的逻辑, 下文讨论}const newChildren = newProps.children;reconcileChildren(current, workInProgress, newChildren, renderLanes);return workInProgress.child;}
updateContextProvider()在fiber初次创建时十分简单, 仅仅就是保存了pendingProps.value做为context的最新值, 之后这个最新的值用于供给消费.
context._currentValue 存储
注意updateContextProvider -> pushProvider中的pushProvider(workInProgress, newValue):
// ...省略无关代码export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {const context: ReactContext<T> = 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 算法之栈操作中有详细图解.
消费 Context
使用了MyContext.Provider组件之后, 在fiber树构造过程中, context 的值会被ContextProvider类型的fiber节点所更新. 在后续的过程中, 如何读取context._currentValue?
在react中, 共提供了 3 种方式可以消费Context:
使用
MyContext.Consumer组件: 用于JSX. 如,<MyContext.Consumer>(value)=>{}</MyContext.Consumer>beginWork中, 对于ContextConsumer类型的节点, 对应的处理函数是updateContextConsumer
function updateContextConsumer(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,) {let context: ReactContext<any> = workInProgress.type;const newProps = workInProgress.pendingProps;const render = newProps.children;// 读取contextprepareToReadContext(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后, 会调用prepareToReadContext - 无论constructClassInstance,mountClassInstance, updateClassInstance内部都调用
context = 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 listdependencies.firstContext = null;}}}// ... 省略无关代码export function readContext<T>(context: ReactContext<T>,observedBits: void | number | boolean,): T {const contextItem = {context: ((context: any): ReactContext<mixed>),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. 返回 currentValuereturn 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树构造直到全部完成即可.
更新 Context
来到更新阶段, 同样进入updateContextConsumer
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;pushProvider(workInProgress, newValue);if (oldProps !== null) {// 更新阶段进入const oldValue = oldProps.value;// 对比 newValue 和 oldValueconst 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<mixed>,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中依赖的contextif (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组件一定执行renderenqueueUpdate(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属性, 表明其子节点有改动, 子节点会进入更新逻辑.- 这一步通过调用scheduleWorkOnParentPath(fiber.return, renderLanes)实现.
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 树构造(对比更新)
- 这一步通过调用scheduleWorkOnParentPath(fiber.return, renderLanes)实现.
通过以上 2 个步骤, 保证了所有消费该context的子节点都会被重新构造, 进而保证了状态的一致性, 实现了context更新.
总结
Context的实现思路还是比较清晰, 总体分为 2 步.
- 在消费状态时,
ContextConsumer节点调用readContext(MyContext)获取最新状态. - 在更新状态时, 由
ContextProvider节点负责查找所有ContextConsumer节点, 并设置消费节点的父路径上所有节点的fiber.childLanes, 保证消费节点可以得到更新.
