• react context原理


    带着问题思考:

    • 1 Provder 如何传递 context?
    • 2 三种获取 context 原理 ( Consumer, useContext,contextType )?
    • 3 消费 context 的组件,context 改变,为什么会订阅更新 (如何实现) 。
    • 4 context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。
    • 5 如何实现的 context 嵌套传递 ( 多个 Povider )?

    context对象

    首先context对象是通过React.createContext创建的,我们看看这个api
    在这里插入图片描述

    • 返回的就是一个对象,上面有Provider属性,本质其实就 Provider的elemetn类型。
    • 而Consumer更是直接指向本身,本质就是一个Context的element类型。
    • 上面还有其他的属性,比如_currentValue就是用来存放context的value的。

    Provider

    上面我们知道了provider就是一个特殊的react Element类型。那么我们重点看下Provider的实现原理。
    围绕着两个点

    • Provider如何传递context状态。
    • Provider中value改变,如何订阅通知context。

    首先看下demo
    在这里插入图片描述
    然后看看App执行beginWork,为provider这个儿子创建fiber时候的场景。

    在这里插入图片描述

    • 这个就是Provider组件的vdom,本质也是一个element,只不过type上面有特殊的标识,标识这是一个Provider组件,并且可以通过type._context获取到value值。
    • 最终通过createFiberFromTypeAndProps为provider组件创建fiber的时候,
      在这里插入图片描述
      传入的type是一个对象。在这里插入图片描述
      最终走到这里逻辑,所以他的fiberTag就是ContextProvider在这里插入图片描述
      接着调用createFiber,返回fiber。此时Provider fiber大概长这样
    {
    alternate: null
    elementType: {
    "$$typeof": Symbol("react.provider")
    _context: {}
    }
    firstEffect: null
    lanes: 1
    memoizedProps: null
    mode: 8
    nextEffect: null
    pendingProps: {
    children: {...}
    value: {test: 1}
    }
    ref: null
    return: null
    
    sibling: null
    stateNode: null
    tag: ContextProvider
    
    type: {
    "$$typeof": Symbol("react.provider")
    _context: {}
    }
    updateQueue: null
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    现在我们知道了Provider fiber长啥样了,现在看看当Provider fiber进入beginWork的时候,会走什么逻辑?
    在这里插入图片描述
    对于ContextProvider,会执行updateContextProvider
    在这里插入图片描述
    大概三个步骤:

    • 1 调用pushProvider,他会获取fiber上面的type,获取_context对象,将value赋值到context._currentValue属性上面。Provider就是通过这个来挂在value,即将value挂载到context._currentValue上面。
    • 2 判断新老props的value是否改变,(浅比较),如果改变调用propagateContextChange函数,没改变则停止调和子节点。
    • 3 如果改变,继续向下调和子节点。

    重点看看propagateContextChange函数,它最终会调用propagateContextChange_eager函数

    propagateContextChange_eager

    简化后的函数

    function propagateContextChange_eager(workInprogress, contextn, renderLanes){
    let fiber = workInProgress.child;
     while (fiber !== null) {
        let nextFiber;
         // Visit this fiber.  每个context存放在fiber.dependencies上面
        const list = fiber.dependencies;
        if (list !== null) {
          nextFiber = fiber.child;
    
          let dependency = list.firstContext;
          while (dependency !== null) {
            // 遍历所有context,因为context可能有多个
            // 如果该context是当前变化的context
            if (dependency.context === context) {
              // 如果是类组件
              if (fiber.tag === ClassComponent) {
                const lane = pickArbitraryLane(renderLanes);
                const update = createUpdate(NoTimestamp, lane);
                update.tag = ForceUpdate;
                // 创建Update,并且将他标记为forceUpdate
              
    
                // 插入fiber.updateQueue钟
                const updateQueue = fiber.updateQueue;
                if (updateQueue === null) {
                  // Only occurs if the fiber has been unmounted.
                } else {
                  const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
                  const pending = sharedQueue.pending;
                  if (pending === null) {
                    // This is the first update. Create a circular list.
                    update.next = update;
                  } else {
                    update.next = pending.next;
                    pending.next = update;
                  }
                  sharedQueue.pending = update;
                }
              }
    
              // 将当前子节点的fiber的优先级更新
              fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
              const alternate = fiber.alternate;
              if (alternate !== null) {
                alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
              }
              // 向上遍历父级fiber的childLanes
              scheduleContextWorkOnParentPath(
                fiber.return,
                renderLanes,
                workInProgress,
              );
    
              // Mark the updated lanes on the list, too.
              list.lanes = mergeLanes(list.lanes, renderLanes);
    
              // Since we already found a match, we can stop traversing the
              // dependency list.
              break;
            }
            dependency = dependency.next;
          }
          ...}
    
    
     if (nextFiber !== null) {
          nextFiber.return = fiber;
          // 如果nextFiber为null,表示没有子节点,那么就得处理兄弟节点,比如
          //  son1处理之后就得处理son2
        } else {
          // No child. Traverse to next sibling.
          nextFiber = fiber;
          while (nextFiber !== null) {
            if (nextFiber === workInProgress) {
              // We're back to the root of this subtree. Exit.
              nextFiber = null;
              break;
            }
            const sibling = nextFiber.sibling;
            if (sibling !== null) {
              // Set the return pointer of the sibling to the work-in-progress fiber.
              sibling.return = nextFiber.return;
              nextFiber = sibling;
              break;
            }
            // No more siblings. Traverse up.
            nextFiber = nextFiber.return;
          }
        }
        // 遍历条件
        fiber = nextFiber;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 首先,从ProviderFiber.child开始,通过while循环遍历,从每个fiber.dependencies获取一个数组,里面存放着所有的context,因为context可能有多个。
    • 遍历dependencies链表,如果当前context等于变化的context的时候,如果是类组件的话,当前创建一个update,并且将update的类型标记为forceUpdate(类似于调用this.forceUpdate带来的更新)。
    • 更新自身fiber和父级以上的fiber的优先级。
    • 继续遍历,如果没有儿子,那就遍历兄弟的,因为都属于Provider下面的节点。

    这里可以罗列几个问题

    fiber.dependencies是什么?

    首先,fiber.dependencies存放着每个context,一个fiber可以有多个context与之对应,什么情况下会使用context呢?
    1 有contextType静态属性指向的类组件
    2 使用useContext的函数组件
    3 使用了contet提供的Consumer
    这里就可以推测,遇到上述3种的fiber,就会将context放入dependencies种。

    为什么只针对类组件创建一个forceUpdate的update呢?

    类组件如果要强制更新,就得通过PureComponent和shouldComponent等阻碍。而context要想突破这些限制,就比如做到当value改变,直接强制消费context的类组件更新,那么就需要通过forceUpdate了。
    而这也解释了最开始的问题?context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。,就是通过value改变,对于Provider下面的儿孙子们,只要有一个消费了context的类组件,直接创建一个forceUpdate的update。
    在这里插入图片描述

    为什么要遍历父级更新fiber的优先级。

    react更新机制的原因,如果此次更新可能发生在fiber树上某一叶子种,因为context穿透影响,react并不知道此次更新的波及范围。那么如何处理呢?其实跟setState触发更新react重新更新的机制是一样的。

    • react会从RootFiber开始更新,每一个更新fiber都走一次beginWork,然后通过判断当前fiber.childLandes或者fiber.lanes是否等于此次更新的lane,以此来判断当前节点是否需要更新。
    • 有三种情况,
      1 如果遇到组件,但是fiber.childLanes和lanes都不等于,也就是说当前更新不涉及到该fiber所在的链上,那么就不会render,也不会向下beginWork。
      2 如果遇到组件,fiber.lanes不等于,但是fiber.childLanes等于,也就是说当前更新属于该节点下面的某个节点。那么就不render,但是会向下beginWork,目的明显,就是为了找到对应的更新组件。
      3 如果遇到hostComoonent如div,那么直接判断childLanes,如果不等于就退出,等于的话就继续向下beginWork。

    以此我们就知道了,为什么当前fiber context变化的话,需要更新从该ifber到rootFiber上所有fiber的优先级,为的就是方便react更新的时候能顺利找到发生变化的fiber。
    总结:
    1 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
    2 render 不等于 beginWork,但是 render 发生,一定触发了 beginwork 。
    3 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。

    Context更新的原理

    只要Porvider上面的context发生变化,就会递归所有的子组件,只要是消费了context的fiber,都会给一个高优先级,并且向上更新父级fiber的优先级,然后react从rootFiber往下遍历,直到找到该fiber,进行更新。图所示:

    发现context变化,类组件消费Context,提高优先级
    在这里插入图片描述
    react从rootFIber往下遍历,找到变化的fiber。
    在这里插入图片描述

    Consumer原理

    上述说到了,Consumer其实就是context对象本身,而context对象本身就是一个element镀锡,类型为React_CONTEXT_TYPE。那么看看该对象作为组建的话,在beginWork的操作。
    对于COnsumer组件,
    在这里插入图片描述
    他的fiberTag就是ContextConsumer,对应的在beginWork阶段调用的函数就是
    在这里插入图片描述

    function updateContextConsumer(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ) {
      let context: ReactContext<any> = workInProgress.type; //获取context对象
      context = (context: any)._context; //获取context对象
      const newProps = workInProgress.pendingProps; //即将更新的props
      const render = newProps.children; //得到render , consumer的children就是一个函数,参数就是value
    
       /* 读取 context */ 
      prepareToReadContext(workInProgress, renderLanes); 
      // 通过context对象获取到最新的value
      const newValue = readContext(context);
    
      let newChildren;
        // 将新的context通过props传给render,得到最新的vdom
        newChildren = render(newValue);
    
      // React DevTools reads this flag.
      workInProgress.flags |= PerformedWork; //打上标记
    
      // 开始根据新的子vdom调和子fiber
      reconcileChildren(current, workInProgress, newChildren, renderLanes);
      // 返回儿子
      return workInProgress.child;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    如上所示,其实做的事情就是

    • 获取context,调用readContext获取最新的value。
    • 然后通过render(value)将value作为参数传入,因为Consumer的children就是一个函数。得到children
    • 继续调和children。

    上面说到fiber是如何与context建立关联的,其实就是通过readContext。
    在这里插入图片描述
    如上,创建一个contextItem,多个contextItem通过链表关联。然后存放在fiber.dependencies上面,以此达到fiber和contex之间的联系。这样下次Provider更新的时候,才能遍历子孙组件,通过对比子孙组建的dependencies上面的context,找到需要更新的fiber,进行更新。

    看完了Conusmer的原理,我们需要再了解一下contextType和useContext的原理。
    其实也很简单

    useContext

    在这里插入图片描述
    在保存hooks的对象中我们可以看到,useContext就是readContext,我们需要显示的传入context对象,才能获取value,并且readContext也会将该函数组件的fiber.dependencies和当前context建立关联,只要value改变,Provdier组件进行beginWork的时候,也能找到该函数组件进行标记,促使其渲染。

    contextType原理

    其原理和useContext一样,本质也是调用readContet在这里插入图片描述
    类组件创建实例的时候,如果遇到了有静态属性contextType的,就直接调用readContext,然后也会将该类组件建立与当前context的关系,方便更新。

    Provider 嵌套传递原理。

    知道了ifber怎么存放context对象之后,多个Provider嵌套的原理其实也明白了。多个provider嵌套的话,如果有订阅的,就会建立关联,多个context对象同时共存于fiber.dependience,然后该怎么更新就怎么更新。因为每个context跟fiber的关联逻辑就在那。并不影响。

    Context流程总结
    • 首先看createContext函数,返回了一个context对象,value值存放在了_currentValue上,而还提供了Provider对象和Consumer对象,本质也是react elemetn对象。

    • 其次对于Provider组件,每次进行beginWork的时候,都会判断当前的value是否改变,如果改变了,那么他会遍历所有的子孙fiber,如果遇到了消费context的fiber(通过获取fiber.dependencies上的contextItem对象),如果是类组件,那么直接创建一个forceUpdate的update,为的就是避免PureComoent和shouldComponentUpdate的影响。如果是其他组件,比如函数组件,或者是Consumer组件,就会提升其优先级,并且将fiber到rootFiber所有的fiber的chilLanes也提升优先级。

    • 对于Context的订阅一共有三种,useContext, Consumer, contextType,本质上都是调用readContext来建立fiber与context的关联(context对象通过链表存放在fiber.dependience上面),然后返回最新的value。

    • 只有订阅了context的fiber,才会建立关联,那么value改变的时候,Provider组件进行beginWork的时候才能找到订阅了context的fiber,而对其他没有订阅的fiber不会影响。

    • 文章通过学习掘金的《react进阶实践指南》作为笔记产出,文中图片部分来自《react进阶实践指南》

  • 相关阅读:
    python树状打印项目路径
    记录:微星 GE63 屏轴断裂 之后。。。
    Halon 模板匹配流程
    微信小程序:云开发表白墙微信小程序源码下载免服务器和域名支持流量主收益
    算法 学习杂谈
    蓝桥等考Python组别四级003
    Python---多线程
    [老文拾遗]如果我当上技术经理如何展开工作(三)
    数字化医院信息云平台源代码 HIS系统全套成品源代码
    Go runtime 调度器精讲(十一):总览全局
  • 原文地址:https://blog.csdn.net/lin_fightin/article/details/128048654