React源码 - React Context 原理

·  阅读 367

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

简单来讲, 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树渲染时, 在beginWorkContextProvider类型的节点对应的处理函数是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 Apifiber树构造过程中的作用. 有关pushProvider/popProvider的具体实现过程(栈存储), 在React 算法之栈操作中有详细图解.

消费 Context

使用了MyContext.Provider组件之后, 在fiber树构造过程中, context 的值会被ContextProvider类型的fiber节点所更新. 在后续的过程中, 如何读取context._currentValue?

react中, 共提供了 3 种方式可以消费Context:

  1. 使用MyContext.Consumer组件: 用于JSX. 如, <MyContext.Consumer>(value)=>{}</MyContext.Consumer>

    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都直接调用了readeContext
  3. class组件中, 使用一个静态属性contextType: 用于class组件中获取context. 如, MyClass.contextType = MyContext;

所以这 3 种方式只是react根据不同使用场景封装的api, 内部都会调用prepareToReadContextreadContext(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;
        }
      }
      复制代码
    • scheduleWorkOnParentPathmarkUpdateLaneFromFiberToRoot的作用相似, 具体可以回顾fiber 树构造(对比更新)

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

总结

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

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