图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

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;
// 接收新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的最新值, 之后这个最新的值用于供给消费.

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:

  1. 使用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;
    // 读取context
    prepareToReadContext(workInProgress, renderLanes);
    const newValue = readContext(context, newProps.unstable_observedBits);
    let newChildren;
    // ...省略无关代码
    }
  2. 使用useContext: 用于function中. 如, const value = useContext(MyContext)

    • 进入updateFunctionComponent后, 会调用prepareToReadContext
    • 无论是初次创建阶段, 还是更新阶段, useContext都直接调用了readContext
  3. 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 list
dependencies.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. 返回 currentValue
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}

核心逻辑:

  1. prepareToReadContext: 设置currentlyRenderingFiber = workInProgress, 并重置lastContextDependency等全局变量.
  2. 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 和 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);
}
}
// ... 省略无关代码
}

核心逻辑:

  1. value没有改变, 直接进入Bailout(可以回顾fiber 树构造(对比更新)中对bailout的解释).
  2. value改变, 调用propagateContextChange

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中依赖的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源码比较长, 核心逻辑如下:

  1. 向下遍历: 从ContextProvider类型的节点开始, 向下查找所有fiber.dependencies依赖该context的节点(假设叫做consumer).

  2. 向上遍历: 从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 树构造(对比更新)

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

总结

Context的实现思路还是比较清晰, 总体分为 2 步.

  1. 在消费状态时,ContextConsumer节点调用readContext(MyContext)获取最新状态.
  2. 在更新状态时, 由ContextProvider节点负责查找所有ContextConsumer节点, 并设置消费节点的父路径上所有节点的fiber.childLanes, 保证消费节点可以得到更新.