《深入浅出react开发指南》总结之 10.2 渲染阶段流程探秘

123 阅读8分钟

10.1我们提到 renderRootSync 是调和过程中的渲染阶段,下面来详细讲解一下

fiber更新循环 workLoop

来看函数体:

function renderRootSync(root,lanes){
     workLoopSync();
     /* workLoop 完毕,证明已遍历所有节点,那么重置状态,进入 commit 阶段 */
     workInProgressRoot = null;
     workInProgressRootRenderLanes = NoLanes;
}

首先我们来解释下本文最重要的一个概念-- workLoop

我们先来看下实现:

function workLoopSync() {
    /* 循环执行 performUnitOfWork,一直到 workInProgress 为空 */
    while (workInProgress ! = = null) {
     performUnitOfWork(workInProgress);
    }
}

书中有这么一段比喻:

我们用一个例子来形容 workLoop,假设将应用看作一台设备,每一次更新看作一次检修维护,维 修师傅应该如何检修呢?维修师傅会用一台机器 (workLoop 可以看作这台机器),依次检查每一个需 要维护更新的零件 (fiber 可以看作零件),每一个需要检修的零件会进入检查流程(performUnitOfWork)。

如果需要更新, 那么会更新;如果有子零件更新 (子代 fiber),父代本身也会进入机器运转 (workLoop)流程中。

workLoop 是一个工作循环:

  • 在初始化过程中,fiber 树通过 workLoop 过程去构建。
  • 在一次更新中,并不是所有的 fiber 会进入workLoop 中,而是每一个待更新或者有 Child 需要更新的 fiber,会进入 workLoop 流程。
  • workLoop 在整个渲染流程中的角色非常重要,可以将 workLoop 当作一个循环运作的加工器,每一个需要调和的 fiber 可以当作一个零件,每一个零件需要进入加工器,如果没有待加工的零件,加工器才停止运转。

workInProgress 永远指向正在被调和的fiber, workLoop 的执行单元就是 workInProgress fiber 节点,而且更新每一个 fiber 的函数叫作 performUnitOfWork。

我们看下 performUnitOfWork 都干啥了?

function performUnitOfWork(unitOfWork){
     /* 执行 */
     let next = beginWork(current, unitOfWork, subtreeRenderLanes);
     unitOfWork.memoizedProps = unitOfWork.pendingProps;
     /* 优先将 next 赋值给 workInProgress,如果没有 next,那么调用 completeUnitOfWork 向上归并处理。 */
     if (next = = = null) {
     completeUnitOfWork(unitOfWork);
     } else {
     workInProgress = next;
     }
}
  • beginWork:是向下调和的过程。就是由 fiberRoot 按照 Child 指针逐层向下调和,期间会执行函数组件、实例类组件、diff 调和子节点,打不同的标签。
  • completeUnitOfWork:是向上归并的过程,如果有兄弟节点,会返回 sibling 兄弟,没有返回 return父级,一直返回到 fiberRoot,对于初始化流程会创建 DOM,对于 DOM 元素进行事件收集,处理 style、 className 等。
  • 这样一上一下,完成了整个 fiber 树的 workLoop 流程。

function Index(){
 return <div>
     Hello,world
     <p > Let us learn React! </p>
     <button >Go</button>
  </div>
}

在初始化或者一次更新中调和顺序是怎样的呢?beginWork 和 completeUnitOfWork 的执行先后顺序:

  1. beginWork → rootFiber
  2. beginWork → Index fiber
  3. beginWork → div fiber
  4. beginWork → hello,world fiber
  5. completeWork → hello,world fiber (completeWork 返回 sibling)
  6. beginWork → p fiber
  7. completeWork → p fiber
  8. beginWork → button fiber
  9. completeWork → button fiber (此时没有 sibling,返回 return)
  10. completeWork → div fiber
  11. completeWork → Index fiber
  12. completeWork → rootFiber (完成整个 workLoop)

image.png

最小的更新单元

先说结论:

  • fiber 是调和过程中的最小单元,每一个需要调和的 fiber 会进入 workLoop 中。
  • 而组件是最小的更新单元,React 的更新源于数据层 state 的变化。

怎么理解呢?

首先我们知道 fiber的类型有很多种

有 function Component类型的,有 Class Component类型的,还有 hostComponent 类型的fiber(普通div元素这种)

哪些类型fiber 可以触发更新呢?

我们知道 react终究是 数据驱动视图,这个数据又是什么呢?可以是父组件传进来的props,可以是 组件自身的state,可以是 Context数据,但终究是来自某个组件的 state 变化

而state是建立在组件类型的fiber上的,所以只有组件能触发更新 ,hostComponent类型(div元素)fiber是无法触发更新的,但是 hostComponent类型 fiber也是会进入调和阶段的

下面的例子可以加深理解

image.png

场景一:更新 A 组件 state,A 触发更新,如果 B、C 没有做渲染控制处理 (比如 memo PureComponent),那么更新会影响 B、C,而 A、B、C 会重新渲染。

image.png

场景二:当更新 B 组件,那么组件 A fiber 会被标记,然后 A 会调和,但是不会重新渲染;组件B 是当事人,既会进入调和,也会重新渲染;组件 C 受到父组件 B 的影响,会重新渲染。

image.png

场景三:当更新 C 组件,那么 A、B 会进入调和流程,但是不会重新渲染,C 是当事人,会调和并 重新渲染。

从 beginWork 到组件更新全流程

/**
* @param {*} current current 树 fiber
* @param {*} workInProgress workInProgress 树 fiber
* @param {*} renderLanes 当前的渲染优先级
* @returns
*/function beginWork(current,workInProgress,renderLanes){
 /* -------------------第一部分-------------------- */
 if(current ! = = null){
     /* 更新流程 */
     /* current 树上上一次渲染后的 props */
     const oldProps = current.memoizedProps;
     /* workInProgress 树上这一次更新的 props */
     const newProps = workInProgress.pendingProps;
     if(oldProps ! = = newProps || hasLegacyContextChanged()){
         didReceiveUpdate = true;
     }else{ /* props 和 Context 没有发生变化,检查是否更新来自自身或者 Context 的改变 */
         const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
         if(!hasScheduledUpdateOrContext){
             didReceiveUpdate = false;
             return attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
         }
         /* 这里省略了一些判断逻辑 */
         didReceiveUpdate = false;
     }
 }else{
     didReceiveUpdate = false
 }
 /* -------------------第二部分-------------------- */
 /* TODO: 走到这里会触发组件更新,比如函数执行,类组件会执行渲染。*/
 switch(workInProgress.tag){
 /* 函数组件的情况 */
     case FunctionComponent: {
         return updateFunctionComponent (current, workInProgress, Component, resolvedProps, renderLanes)
     }
     /* 类组件的情况 */
     case ClassComponent:{
         return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
     }
     /* 元素类型 fiber <div>, <span> */
     case HostComponent:{
         return updateHostComponent(current, workInProgress, renderLanes)
     }
     /* 其他 fiber 情况 */
 }
}

接下来我们来看下 一个变量的意义:didReceiveUpdate

1. 第一阶段

这个阶段非常重要,也就是判断更新情况。前面的三种场景可以在第一阶段进行判断处理。先来 分析一下第一阶段做了哪些事。正式讲解之前,先来看一个变量的意义,也就是 didReceiveUpdate。

didReceiveUpdate:这个变量主要证明当前更新是否来源于父级的更新,那么自身并没有更新。

比如下图,更新 A 组件,那么 B 组件也会跟着更新,这个情况下 didReceiveUpdate = true。

image.png 首先通过 current!= = null 来判断当前 fiber 是否创建过,如果第一次 mounted,那么 current 为 null,而第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,到第二阶段。

首先我们看看哪些情况会触发 didReceiveUpdate = false,即更新不来自父组件(oldProps = = = newProps)

  • 情况一:还是回到上面的场景,如果 C 组件更新,那么 B 组件被标记 ChildLanes,会进入 beginWork 调和阶段,但是 B 组件本身 props 不会发生变化。即子组件更新,父组件会发生调和,进入 beginWork,父组件跳出调和,返回子组件继续下面的流程

image.png

  • 情况二:通过 useMemo 等方式缓存了 React element 元素,在渲染控制章节讲到过。

  • 情况三:就是更新发生在当前组件本身,比如 B 组件发生更新,但是 B 组件的 props 并没有发生 变化,所以也会走到这个流程上。比如B自身 state发生变化

  • 情况四:初始渲染

我们如何区分 情况一 和 情况三呢,因为显然 情况一不需要走后面的渲染流程,但情况三需要,所以单用 didReceiveUpdate 来区分是不够的

checkScheduledUpdateOrContext就来给您区分了

function checkScheduledUpdateOrContext(current,renderLanes){
     const updateLanes = current.lanes;
     /* 这种情况说明当前更新 */
     if (includesSomeLane(updateLanes, renderLanes)) {
         return true;
     }
     /* 如果该 fiber 消费了 Context,并且 Context 发生了改变。*/
     if (enableLazyContextPropagation) {
         const dependencies = current.dependencies;
         if (dependencies ! = = null && checkIfContextChanged(dependencies)) {
             return true;
         }
     }
    return false;
}

如何判断更新来自自身呢? 答案:检查当前 fiber 的 lane 是否等于 renderLanes(当前的渲染优先级),如果相等,那么证明更 新来源于当前 fiber,比如 B 组件发生更新,会走这里 (情况三)。

所以 checkScheduledUpdateOrContext() 返回 false 就包含 情况一

其实 checkScheduledUpdateOrContext() 返回 false 还有一种情况,就是F触发更新,但如果当前fiber为A时,这种情况 其实代表整个左边子树都可以跳过 调和,-- 情况五

image.png

所以呢,还需要分别处理情况一和情况五 attemptEarlyBailoutIfNoScheduledUpdate 就是干这个的,

attemptEarlyBailoutIfNoScheduledUpdate 最重要的是调用bailoutOnAlreadyFinishedWork。

function bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes){
     /* 如果 Children 没有高优先级的任务,说明所有的 Child 没有更新,那么直接返回,Child 也不会被调和
    */
     if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
         /* 这里做了流程简化 */
         return null
     }
     /* 当前 fiber 没有更新。但是它的 Children 需要更新。 */
     cloneChildFibers(current, workInProgress);
     return workInProgress.child;
}

首先通过 includesSomeLane 判断 ChildLanes 是不是高优先级任务,如果不是,那么所有子孙 fiber不需要调和,直接返回 null,Child 也不会被调和。这就是针对 情况5的处理

如果 ChildLanes 优先级高,那么说明 Child 需要被调和,但是当前组件不需要,所以会克隆一下 Children,返回 Children,本身不会重新渲染。这就是针对 情况一的处理

bailoutOnAlreadyFinishedWork 流程非常重要, 为什么呢?我理解,其实这五种情况,情况一,情况五 的处理对于 react 的性能 至关重要,因为它可以提前return,跳过改fiber的渲染,它可以减少fiber的不必要渲染,甚至是不必要的 调和,大大节省了时间。

2. 第二阶段

从 beginWork 的源码中可以看到,第二阶段就是更新 fiber,比如函数组件,就会调用 updateFunctionComponent,类组件就会调用 updateClassComponent,然后进行重新渲染。

3. 流程总结

最后作者 又贴心的针对我们上面列举的情况 再次总结说明,这里不再赘述了

最后总结

这节内容还是需要好好消化的,尤其是 workloop 概念,以及 react对情况一 和情况五的 特殊处理,我觉得是react 架构的一大亮点。