react context原理

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

现在我们知道了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,表示没有子节点,那么就得处理兄弟节点,比如
      // <Provider><Son1/><Son2/></Provider> 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;

  • 首先,从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;

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

  • 获取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进阶实践指南》

以上是关于react context原理的主要内容,如果未能解决你的问题,请参考以下文章

react context原理

React Context API似乎重新渲染每个组件

React Context 原理理解

React Context 原理理解

React Context : 从 API 获取数据并在 React 组件中发生某些事件时调用 API

[react] 为什么React并不推荐我们优先考虑使用Context?