• 【React 源码】(十五)React Context 原理


    简单来讲, Context提供了一种直接访问祖先节点上的状态的方法, 避免了多级组件层层传递props.

    有关Context的用法, 请直接查看官方文档, 本文将从fiber树构造的视角, 分析Context的实现原理.

    创建 Context

    根据官网示例, 通过React.createContext这个 api 来创建context对象. 在createContext中, 可以看到context对象的数据结构:

     
    
    1. export function createContext(
    2. defaultValue: T,
    3. calculateChangedBits: ?(a: T, b: T) => number,
    4. ): ReactContext {
    5. if (calculateChangedBits === undefined) {
    6. calculateChangedBits = null;
    7. }
    8. const context: ReactContext = {
    9. $$typeof: REACT_CONTEXT_TYPE,
    10. _calculateChangedBits: calculateChangedBits,
    11. // As a workaround to support multiple concurrent renderers, we categorize
    12. // some renderers as primary and others as secondary. We only expect
    13. // there to be two concurrent renderers at most: React Native (primary) and
    14. // Fabric (secondary); React DOM (primary) and React ART (secondary).
    15. // Secondary renderers store their context values on separate fields.
    16. _currentValue: defaultValue,
    17. _currentValue2: defaultValue,
    18. _threadCount: 0,
    19. Provider: (null: any),
    20. Consumer: (null: any),
    21. };
    22. context.Provider = {
    23. $$typeof: REACT_PROVIDER_TYPE,
    24. _context: context,
    25. };
    26. context.Consumer = context;
    27. return context;
    28. }

    createContext核心逻辑:

    • 其初始值保存在context._currentValue(同时保存到context._currentValue2. 英文注释已经解释, 保存 2 个 value 是为了支持多个渲染器并发渲染)
    • 同时创建了context.Providercontext.Consumer2 个reactElement对象.

    比如, 创建const MyContext = React.createContext(defaultValue);, 之后使用声明一个ContextProvider类型的组件.

    fiber树渲染时, 在beginWorkContextProvider类型的节点对应的处理函数是updateContextProvider:

     
    
    1. function beginWork(
    2. current: Fiber | null,
    3. workInProgress: Fiber,
    4. renderLanes: Lanes,
    5. ): Fiber | null {
    6. const updateLanes = workInProgress.lanes;
    7. workInProgress.lanes = NoLanes;
    8. // ...省略无关代码
    9. switch (workInProgress.tag) {
    10. case ContextProvider:
    11. return updateContextProvider(current, workInProgress, renderLanes);
    12. case ContextConsumer:
    13. return updateContextConsumer(current, workInProgress, renderLanes);
    14. }
    15. }
    16. function updateContextProvider(
    17. current: Fiber | null,
    18. workInProgress: Fiber,
    19. renderLanes: Lanes,
    20. ) {
    21. // ...省略无关代码
    22. const providerType: ReactProviderType = workInProgress.type;
    23. const context: ReactContext = providerType._context;
    24. const newProps = workInProgress.pendingProps;
    25. const oldProps = workInProgress.memoizedProps;
    26. // 接收新value
    27. const newValue = newProps.value;
    28. // 更新 ContextProvider._currentValue
    29. pushProvider(workInProgress, newValue);
    30. if (oldProps !== null) {
    31. // ... 省略更新context的逻辑, 下文讨论
    32. }
    33. const newChildren = newProps.children;
    34. reconcileChildren(current, workInProgress, newChildren, renderLanes);
    35. return workInProgress.child;
    36. }

    updateContextProvider()fiber初次创建时十分简单, 仅仅就是保存了pendingProps.value做为context的最新值, 之后这个最新的值用于供给消费.

    context._currentValue 存储

    注意updateContextProvider -> pushProvider中的pushProvider(workInProgress, newValue):

     
    
    1. // ...省略无关代码
    2. export function pushProvider(providerFiber: Fiber, nextValue: T): void {
    3. const context: ReactContext = providerFiber.type._context;
    4. push(valueCursor, context._currentValue, providerFiber);
    5. context._currentValue = nextValue;
    6. }

    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. 如, (value)=>{}

       
      1. function updateContextConsumer(
      2. current: Fiber | null,
      3. workInProgress: Fiber,
      4. renderLanes: Lanes,
      5. ) {
      6. let context: ReactContext = workInProgress.type;
      7. const newProps = workInProgress.pendingProps;
      8. const render = newProps.children;
      9. // 读取context
      10. prepareToReadContext(workInProgress, renderLanes);
      11. const newValue = readContext(context, newProps.unstable_observedBits);
      12. let newChildren;
      13. // ...省略无关代码
      14. }

    2. 使用useContext: 用于function中. 如, const value = useContext(MyContext)

      • 进入updateFunctionComponent后, 会调用prepareToReadContext
      • 无论是初次创建阶段, 还是更新阶段useContext都直接调用了readContext
    3. class组件中, 使用一个静态属性contextType: 用于class组件中获取context. 如, MyClass.contextType = MyContext;

    所以这 3 种方式只是react根据不同使用场景封装的api, 内部都会调用prepareToReadContextreadContext(contextType).

     
    
    1. // ... 省略无关代码
    2. export function prepareToReadContext(
    3. workInProgress: Fiber,
    4. renderLanes: Lanes,
    5. ): void {
    6. // 1. 设置全局变量, 为readContext做准备
    7. currentlyRenderingFiber = workInProgress;
    8. lastContextDependency = null;
    9. lastContextWithAllBitsObserved = null;
    10. const dependencies = workInProgress.dependencies;
    11. if (dependencies !== null) {
    12. const firstContext = dependencies.firstContext;
    13. if (firstContext !== null) {
    14. if (includesSomeLane(dependencies.lanes, renderLanes)) {
    15. // Context list has a pending update. Mark that this fiber performed work.
    16. markWorkInProgressReceivedUpdate();
    17. }
    18. // Reset the work-in-progress list
    19. dependencies.firstContext = null;
    20. }
    21. }
    22. }
    23. // ... 省略无关代码
    24. export function readContext(
    25. context: ReactContext,
    26. observedBits: void | number | boolean,
    27. ): T {
    28. const contextItem = {
    29. context: ((context: any): ReactContext),
    30. observedBits: resolvedObservedBits,
    31. next: null,
    32. };
    33. // 1. 构造一个contextItem, 加入到 workInProgress.dependencies链表之后
    34. if (lastContextDependency === null) {
    35. lastContextDependency = contextItem;
    36. currentlyRenderingFiber.dependencies = {
    37. lanes: NoLanes,
    38. firstContext: contextItem,
    39. responders: null,
    40. };
    41. } else {
    42. lastContextDependency = lastContextDependency.next = contextItem;
    43. }
    44. // 2. 返回 currentValue
    45. return isPrimaryRenderer ? context._currentValue : context._currentValue2;
    46. }

    核心逻辑:

    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

     
    
    1. function updateContextProvider(
    2. current: Fiber | null,
    3. workInProgress: Fiber,
    4. renderLanes: Lanes,
    5. ) {
    6. const providerType: ReactProviderType = workInProgress.type;
    7. const context: ReactContext = providerType._context;
    8. const newProps = workInProgress.pendingProps;
    9. const oldProps = workInProgress.memoizedProps;
    10. const newValue = newProps.value;
    11. pushProvider(workInProgress, newValue);
    12. if (oldProps !== null) {
    13. // 更新阶段进入
    14. const oldValue = oldProps.value;
    15. // 对比 newValue 和 oldValue
    16. const changedBits = calculateChangedBits(context, newValue, oldValue);
    17. if (changedBits === 0) {
    18. // value没有变动, 进入 Bailout 逻辑
    19. if (
    20. oldProps.children === newProps.children &&
    21. !hasLegacyContextChanged()
    22. ) {
    23. return bailoutOnAlreadyFinishedWork(
    24. current,
    25. workInProgress,
    26. renderLanes,
    27. );
    28. }
    29. } else {
    30. // value变动, 查找对应的consumers, 并使其能够被更新
    31. propagateContextChange(workInProgress, context, changedBits, renderLanes);
    32. }
    33. }
    34. // ... 省略无关代码
    35. }

    核心逻辑:

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

    propagateContextChange:

     
    
    1. export function propagateContextChange(
    2. workInProgress: Fiber,
    3. context: ReactContext,
    4. changedBits: number,
    5. renderLanes: Lanes,
    6. ): void {
    7. let fiber = workInProgress.child;
    8. if (fiber !== null) {
    9. // Set the return pointer of the child to the work-in-progress fiber.
    10. fiber.return = workInProgress;
    11. }
    12. while (fiber !== null) {
    13. let nextFiber;
    14. const list = fiber.dependencies;
    15. if (list !== null) {
    16. nextFiber = fiber.child;
    17. let dependency = list.firstContext;
    18. while (dependency !== null) {
    19. // 检查 dependency中依赖的context
    20. if (
    21. dependency.context === context &&
    22. (dependency.observedBits & changedBits) !== 0
    23. ) {
    24. // 符合条件, 安排调度
    25. if (fiber.tag === ClassComponent) {
    26. // class 组件需要创建一个update对象, 添加到updateQueue队列
    27. const update = createUpdate(
    28. NoTimestamp,
    29. pickArbitraryLane(renderLanes),
    30. );
    31. update.tag = ForceUpdate; // 注意ForceUpdate, 保证class组件一定执行render
    32. enqueueUpdate(fiber, update);
    33. }
    34. fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
    35. const alternate = fiber.alternate;
    36. if (alternate !== null) {
    37. alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
    38. }
    39. // 向上
    40. scheduleWorkOnParentPath(fiber.return, renderLanes);
    41. // 标记优先级
    42. list.lanes = mergeLanes(list.lanes, renderLanes);
    43. // 退出查找
    44. break;
    45. }
    46. dependency = dependency.next;
    47. }
    48. }
    49. // ...省略无关代码
    50. // ...省略无关代码
    51. fiber = nextFiber;
    52. }
    53. }

    propagateContextChange源码比较长, 核心逻辑如下:

    1. 向下遍历: 从ContextProvider类型的节点开始, 向下查找所有fiber.dependencies依赖该context的节点(假设叫做consumer).
    2. 向上遍历: 从consumer节点开始, 向上遍历, 修改父路径上所有节点的fiber.childLanes属性, 表明其子节点有改动, 子节点会进入更新逻辑.
      • 这一步通过调用scheduleWorkOnParentPath(fiber.return, renderLanes)实现.
         
        1. export function scheduleWorkOnParentPath(
        2. parent: Fiber | null,
        3. renderLanes: Lanes,
        4. ) {
        5. // Update the child lanes of all the ancestors, including the alternates.
        6. let node = parent;
        7. while (node !== null) {
        8. const alternate = node.alternate;
        9. if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
        10. node.childLanes = mergeLanes(node.childLanes, renderLanes);
        11. if (alternate !== null) {
        12. alternate.childLanes = mergeLanes(
        13. alternate.childLanes,
        14. renderLanes,
        15. );
        16. }
        17. } else if (
        18. alternate !== null &&
        19. !isSubsetOfLanes(alternate.childLanes, renderLanes)
        20. ) {
        21. alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
        22. } else {
        23. // Neither alternate was updated, which means the rest of the
        24. // ancestor path already has sufficient priority.
        25. break;
        26. }
        27. node = node.return;
        28. }
        29. }

      • scheduleWorkOnParentPathmarkUpdateLaneFromFiberToRoot的作用相似, 具体可以回顾fiber 树构造(对比更新)

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

    总结

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

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

  • 相关阅读:
    python爬取酷我音乐 根据歌名进行爬取
    爬手机app接口数据,手机不能上网问题及解决,
    lssvm聚类研究(Matlab代码实现)
    深入理解线程安全
    UWB系统的定位精度影响因素(一)
    etcd备份恢复原理详解及踩坑实录
    友元 C++进修之路系列
    初识集合框架 -Java
    老卫带你学---leetcode刷题(406. 根据身高重建队列)
    这10款文案神器帮你速码,做自媒体还担心写不出文案吗?
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126545869