4. render

Posted 茂树24

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4. render相关的知识,希望对你有一定的参考价值。

文章目录

4. render

在 scheduler 阶段根据每一个调度任务的优先级(过期时间)筛选出一个调度任务,进入 render 阶段进行构建或者更新 workInProgress 树。render 阶段是对 DOM 对应的 current Root 深度优先遍历后,分别进行了 创建更新 workInProgress、遍历树剪枝操作、执行创建对应的组件、收集Effect、更新孩子过期时间、completeWork 、中断错误挂起处理 等。最终如果没有被其他任务中断的情况下,会进入 commit 阶段同步更新 DOM。
(基于 v16.13.1 版本)

文章原地址:https://www.yuque.com/wmaoshu/react/cy1m6k


render 整体流程

render 整体流程是基于经典的 DFS 算法非递归实现的,在算法中分为访问和回溯两个大的过程,在访问到某个节点的时候,会根据节点的类型执行相应的动作产出 child,在回溯过程中,会收集被回溯节点的孩子节点相关的DOM、effect、expirationTime 等。

同时 render 过程也是基于 double buffer 模式的,所以存在 current workInProgress 两个fiber互相对应, 但是 render 阶段是根据 current 上收集到的副作用、变化更新创建出下一个要 commit 的 fiber 树 — workInProgress。粗略的介绍该创建更新流程。

DFS 框架遍历

整体的 render 是一个 DFS 非递归深度遍历的过程 ,大概整个流程如下代码所示:

const DFS = (root) => 
  while (root) 
    nextRoot = root.child;
    if (nextRoot) 
      // 处理孩子节点
      root = nextRoot;
     else 
      while (true) 
        if (root.sibling) 
          // 处理兄弟节点
          root = root.sibling;
          break;
         else if (root.return) 
          // 回溯找到待需要处理兄弟节点的父节点
          root = root.return;
         else 
          // 整棵树遍历完成
          root = null;
          return;
        
      
    
  
;

先深入到一条通路直到到达叶节点为止,然后回溯找到该通路上的兄弟节点,最终遍历完成整棵树。整个过程分为两个部分 访问 和 回溯。所以可以得到如下的结论:

  • 树中每个节点都经历过访问回溯的过程。
  • 回溯的节点其所有的孩子节点应该都被访问回溯过。
  • 回溯到节点为空时,整棵树回溯完成。
  • 当下一个访问节点为空时,从根节点到该节点的路径上节点都被访问过。

关键处理方法

react 的 render 过程正是使用 DFS 非递归遍历的特性:访问、回溯,分别进行处理使得一趟遍历就会收集到当前调度的所有需要被更新的节点 effect。从而为 commit 阶段提供 effect 更新列表,从而避免了 commit 阶段再一次遍历所有节点进行 effect 收集工作。将 react 源码进行抽象后,得到如下代码:


const beginWork = (current, workInProgress) => 
  // 根据不同的 type 处理不同逻辑
  return workInProgress.child;
;

const completeUnitOfWork = (workInProgress) => 
  while(true) 
    const returnFiber = workInProgress.return;
    const siblingFiber = workInProgress.sibling;

    completeWork(workInProgress);
    resetChildExpirationTime(workInProgress);
    setEffectToReturn(workInProgress);

    if (siblingFiber !== null) 
      return siblingFiber;
     else if (returnFiber !== null) 
      workInProgress = returnFiber;
      continue;
     else 
      return null;
    
  
;

const wookLoop = (workInProgress) => 
  const current = workInProgress.alternate;
  let next;
  next = beginWork(current, workInProgress);
  if(!next) 
    next = completeUnitOfWork(workInProgress);
  
  return next;
;

const renderRoot = (root) => 
  let nextUnitOfWork = root.workInProgress;
  do 
    try 
      nextUnitOfWork = wookLoop(nextUnitOfWork);
     catch (error) 
      // error、suspend
    
  while(true);


renderRoot(root);

这部分代码是 上边 DFS 代码的扩展,其中 beginWork 对应了 访问 的过程,completeUnitOfWork 代表了 回溯 的过程。其中:

  • beginWork 是处理节点生成 child 的过程。比如 函数组件将运行函数得到孩子节点,或者 执行 类组件的 render 方法生成孩子节点;得到的孩子节点将会作为 beginWork 接下来处理的对象。如果 child 是 null 代表该路径已经遍历完成了需要准备回溯,要执行 completeUnitOfWork 了。

  • completeUnitOfWork 是回溯收集元素 DOM 变更,修改 child 过期时间 以及 收集 effect 的过程。并且在这个过程中,已经确保被处理的节点的所有孩子节点都被 beginWork 和 completeUnitOfWork 处理过,也就是都被访问、回溯过。

  • completeWork 是将其所有第一次层孩子节点的 DOM 实例 appendChild 到该节点 DOM 实例上的过程,之所以是第一层,是因为其孩子节点已经完成了他们孩子节点的 appendChild 工作,这个过程接下来会做具体的介绍。

  • resetChildExpirationTime 将所有的孩子节点以及孩子节点上的childExpirationTime比较找出过期时间最短的值,并且重新设置该节点。这个过程也会在下面具体分析。

  • setEffectToReturn 同上两个方法,其过程也是收集该节点所有孩子节点的 effect 的信息,比如回调函数的执行、生命周期方法执行、删除某节点等。为 commit 阶段作为数据源。

  • 当然如果在整个过程中,出现了异常错误,或者时间资源被回收,或者被中断,都会进入到 suspend(挂起) 或者 error(错误) 阶段。还阶段在 render 过程中不做介绍。


源码分析

熟悉了整体流程之后,接下来进入具体的分析阶段,在每个阶段中,react 都会有一定的方式进行优化,使得尽可能的少的遍历整棵树达到收集所有更新的目的,这些因素也是我们写 react 代码,提高 react 执行性能的关键。

前置知识点

介绍一些全局变量

isWorking

在 renderRoot 和 commitRoot 阶段一开始的时候都会设置为 true,他们各自阶段结束重置为 false。目的是为了区分是否在处理某一个调度,如果为 true 说明正在处理某一批量更新也就是某一次调度,可能在render 阶段或者commit 阶段, 如果为 false 说明 render commit 阶段没有执行。

比如在执行 calss 组件调用生命周期方法的过程中触发了 setState 更新, 那么此时的环境 isWorking 是 true 的, 因为执行生命周期方法的时候多半会在 render 过程中执行,所以在 scheduler 阶段 创建调度任务的时候会去判断此时是否处理 working 阶段,如果是的话说明这一次的 setSate 更新应该和当前 render 更新是同一批次的,所以会设置相同的过期时间。

nextRoot & nextRenderExpirationTime

用于记录 render 阶段中被处理的 root 节点以及对应的过期时间,root、renderExpirationTime的值具体事由 scheduler 过程调度绝对需要哪个 root 进入到 render 阶段的。

nextUnitOfWork

用于记录 render 阶段中,在遍历 workInProgress 树的过程中被访问到的 fiber 节点。该变量只会只想 workInProgress阶段的 fiber。如果 nextUnitOfWork 设置为 null, 则说明 current 遍历完成,与之对应的 workInProgress 创建处理完毕。

renderRoot

react采用了 double buffer 的处理方式提高渲染的性能,这种方式能避免切换帧的时候带来的等待 js 代码执行的消耗,同时可以将一个调度周期内所有的更新一次更新到 DOM 上,避免频繁读写 DOM 带来的开销。react 中 current 代表着一个 由 fiber 节点组成的树,该树与 DOM 节点一一对象,也就是 此时看到的画面 DOM 的 react 内部 fiber 表示, 与 current fiber 树 对应的 还有一颗 workInProgress 树,这棵树表示此时 react 正在构建的版本,current 与 workInProgress 之间通过 alternate 节点互相关联。其中 current 与 workInProgress 就是组成了 double buffer 模式,以便于浏览器有空闲要更新下一调度的时候总会不需要花费较长时间的等待。

那么介绍完 double buffer 后,renderRoot函数 操作的对象就是 current fiber 树对应的 workInProgress 树,该函数描述操作了 workInProgress 树的更新创建过程。

renderRoot 在源代码位置:react/packages/react-reconciler/src/ReactFiberScheduler.js 1132

源代码的主要部分流程图大概如下(可以点击查看大图):
![scheduleWork-第 5 页.png](https://img-blog.csdnimg.cn/img_convert/377944ae0d19c98e70fe664c02b48d9c.png#align=left&display=inline&height=731&margin=[object Object]&name=scheduleWork-第 5 页.png&originHeight=2722&originWidth=2991&size=452632&status=done&style=none&width=803)
renderRoot 传入 root 为 scheduler 过程中选出的优先级比较高的需要被执行的 current 树,isYieldy 是指该时间片内也就是一帧内是否还有剩余的时间进行执行,isExpired,该任务是否是一个过期的任务。

开始执行 renderRoot函数,在函数一开始执行时候会设置 isWorking 为 true, 结束的时候会设置为 false。expirationTime之所以要设置为 nextExpirationTimeWorkOn 是因为后者在 接下来的循环中可能会被中断挂起等过程,那么下一次再执行的时候需要有新的过期时间,那么新的过期时间就会保存在这个变量中。这里更新 expirationTime 就是为了处理挂起的场景。进入如下的判断过程:

expirationTime !== nextRenderExpirationTime ||
root !== nextRoot ||
nextUnitOfWork === null

这个判断是,如果此时需要被render 的 root 和上一次处理的 root 不一致,或者时间不一致,或者 上一次并没有任何的 root 处理或者上一次 root 处理完毕(因为 nextUnitOfWork 只有在初始化渲染或者上一次 render 结束的情况下才为 null)。一次 root 的处理过程中 有可能会被中断或者挂起,比如出现错误 异步调度中时间片时间到了的情况都会使得 nextRoot 还保持之前的 root。所以新的 root 需要和上一次被中断的root比较,如果一致则说明可以继续处理不需要重新设置 root, 否则,说明 此时的 root 与上一次 root 不一致,或者优先级不同,需要清空上一次 渲染 root 过程中的环境变量,重新设置新的 root 从头开始执行。所以,只有在 新的 root 与 之前的 root 一致,并且过期时间也一致,并且 之前有 nextUnitOfWork 也就是还没有遍历完的情况下复用,其他的情况都是重新设置后继续。对于情况上一次的任务是使用 resetStack 方法, 这个方法就是讲一些关键的变量清空。

如果重置了上一次渲染的环境变量后,接下来赋值环境变量:

nextRoot = root;
nextRenderExpirationTime = expirationTime;
nextUnitOfWork = createWorkInProgress(
    nextRoot.current,
    null,
    nextRenderExpirationTime,
 );

值得注意的是,render 过程中处理的 workInProgress 的树 都是在这一步 从 current 树中创建更新而来的,当然如果 current 已经存在一个对应的 workInProgress 则会去复用该对象,只不过会重新设置 属性 props 和 过期时间。react 通过这种方式来最大化的复用创建的对象,避免频繁的触动垃圾回收 引起内存抖动最终导致卡顿。

初始化环境设置完成后,会进入 nextUnitWork 循环的过程, 也就是 图片右边 while 的过程,这也是 DFS 算法中的第一层 while 访问每一个子节点的过程。在每一次的处理中,先判断 nextUnitWork 是否是null, 除了被中断挂起以外,只有在回溯到根节点的时候,根节点的父亲为空,所以是 null, 整个大循环才会结束。 但是如果调度的任务是一个异步的话,每访问一个节点都会判断一下当前帧是否还有空闲的时间处理接下来其他的节点,也就是 shouldYield 方法, 如果检测到当前帧还有剩余时间并且没有被其他任务抢占,则可以继续,否则就会直接退出交出浏览器资源 让给优先级更高的动画或者用户交互事件。

在一次循环中会先调用 beginWork 方法, 该方法就是处理 nextUnitWork ,比如 调用类组件的 render 方法,函数组件执行调用等。返回的是该组件返回的新的孩子节点,当然对于不需要更新的部分也是有处理的,会在接下来 beginWork阶段详细的介绍。如果孩子节点是非空,说明这个孩子节点是要继续访问的也就是要继续执行看看是否还产生孩子的孩子。否则, 如果返回的是 null, 说明 此条通路已经完成,需要进入回溯阶段,在回溯中调用 completeUnmiOfWork 方法, 该方法会判断有没有 兄弟节点如果有的话会继续作为 unit 继续 调用 beginWork 方法, 否则逐步向上回溯,直到 回溯到 父节点是 null 为止。

总结:renderRoot 方法对于 root 前后一致并且过期时间一致并且上一次 render 过程没有结束还有 unitWork 需要接着执行的 root 不需要更新渲染环境变量继续执行,其他的情况下都要重新创建从根节点开始渲染。unitWork 都是 workInProgress 节点,在遍历 current 根的时候会调用createWorkInProgress 方法生成根节点对应的 workInProgress 节点,在生成过程中存在的对象将会复用,否则创建新的 workInProgress 对象,目的节省内存避免频繁创建删除带来内存抖动。开始从上到下访问每一个节点,调用 beginWork 方法,如果子节点是 null,会调用 completeOfWork 继续执行兄弟节点还是回溯父节点。对于异步的调度任务,在每一次访问子节点之前都会判断当前帧是否有空余的时间执行,如果没有则交还浏览器资源,等待下一次被调度再一次渲染。

beginWork

beginWork 接受被访问的 workInProgress fiber 节点,然后需要对该节点进行处理,在该过程中,react 采用了一些方式避免对非必须更新的组件进行遍历 达到了一个剪枝的目的。

beginWork

react 源码位置:react/packages/react-reconciler/src/ReactFiberBeginWork.js


进入执行 beginWork 一开始就进入了一个严格的判断,判断是否需要更新执行该节点:

current !== null &&
oldProps === newProps &&  // oldProps 也就是 current.memoizedProps, newProps 也就是 workInProgress.pendingProps
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)

这个逻辑是说 该节点不是第一次创建,并且 props 没有更新,这里是浅对比对比props 引用是否发生改变,因为在后续更新过程中可能会对 props 进行修改。重要的是该节点的 过期时间也就是 updateExpirationTime 如果比 此时的调度任务的过期时间还要长,说明在此次的调度过程中还不需要马上更新该节点。可以等到下一次调度的时候进行。

对于需要更新的来说,根据节点的类型分别调用具体的更新发方法进行更新,比如 类组件是 ClassComponent 类型的等在下面几个小节中分别介绍。而对于不需要更新来说需要调用 下面的 bailoutOnAlreadyFinishedWork 方法。

bailoutOnAlreadyFinishedWork

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 1454

[scheduleWork-第 8 页.png](https://img-blog.csdnimg.cn/img_convert/5df8617bab4d553f4d0f2ee96da3f27f.png#align=left&display=inline&height=656&margin=[object Object]&name=scheduleWork-第 8 页.png&originHeight=792&originWidth=460&size=111509&status=done&style=none&width=381)
对于每一个 Fiber 节点都有 childExpirationTime 来描述该节点的所有孩子节点中最小的过期时间,如果 childExpirationTime 比 调度的时间都要大的话说明该节点的所有的孩子节点都不需要更新直接结束 返回 null。否则,就会将 child 以及 sbiling 节点创建 workInProgress 节点 返回 child ,跳过该节点继续孩子节点的处理。当然这里的创建 workInProgress 和 beginWork 中一样也是调用 createWorkInProgress 也是恢复用创建好了的节点而不会丢弃重新创建。

HostRoot

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 613


在调用 reactDOM.render 中会传入 第一个参数是 reactNode 一般是 , 在render方法中会在根节点的 fiber 对象中的 updatequeue 中添加一个 update, 其中state 参数有element 属性, 这个属性的值就是 第一个参数的 根节点 reactNode 对象。

如果 对比前后根节点的 reactNode 对象没有变化的话,则说明根节点 fiber 不需要更新,则要调用 bailoutOnAlreadyFinishedWork 方法看看后续的孩子节点是否有必要更新。如果 前后的 element 节点不相等说明 更新了, 需要调用 reconcileChildren 方法更新处理子节点,节点的 child 返给 beginWork 继续执行。

reconcileChildren是负责将 reactNode 节点转化成 workInProgress fiber 节点的方法, 一般在一个节点被访问执行之后会将子节点传入该方法具体的创建更新。接下来会有专门的部分介绍该方法。

FunctionComponent

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 397


beginWork 访问到一个 FunctionComponent 类型的组件的时候, 会先执行 resolveDefaultProps 方法,该方法就是讲待传入的 fiber 节点中 pendingProps 和 该函数 下 defaultProps 对象进行一个合并操作,值得注意的是,如果 defaultProps 不存在也就是值为 undefined,则会复用 pendingProps 对象,否则都会创建一个新的对象去接受 合并后的属性。

然后使用 新的 props 参数调用函数方法,返回了 child 节点,该节点既有可能是一个数组,也有可能是一个对象,接下来将该孩子节点 放入到 reconcileChidren 处理。 最终将 返回的child 节点 作为beginWork 的 nextUnitWork 进一步处理。

MemoComponent

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 235

先回顾一下 memo 封装的 函数组件 最终的 reactNode 节点 如下所示:


    $$typeof: REACT_MEMO_TYPE,
    type:  // FunctionComponent
      $$typeof: REACT_ELEMENT_TYPE,
      type: FunctionComponent,
      props: 
      ,
      key: ''
    ,
    compare: (oldProps: Props, newProps: Props) => boolean,
 

经过 memo 包裹处理的 函数组件 得到的 reactNode 具有 type属性, 该属性新的值就是被包裹的函数组件, 当然函数组件也会有 type, 函数组件的type 值是该函数, 所以, memo 处理后的 type.type 其实就是函数本身。给经过 memo 包裹后的组件传入 props 其实也是给被包裹的函数组件。

整个过程和 FunctionComponent 类似,也是一开始需要调用 resolveDefaultProps 方法, 从 type.type 中的带函数组件 获取函数组件的 defaultProps 然后和 传入 memo 组件的 pendingProps 做一个合并操作。

会先判断 memo 对象的 workInProgress fiber 组件中的 current 属性是否存在, 不存在说明是一个新创建的 memo 函数组件。

如果 current 不存在的话,如果是一个简单的函数组件(也就是非类组件并且 defaultProps 不存在)并且 compare 函数也不存在,则说明是一个简单的 memo 函数组件,直接调用上面的 FunctionComponent 进行更新。否则的话,为 函数组件创建一个对应的 fiber, fiber对象的type 此时并非 FunctionComponent 类型 而是 IndeterminateComponent 类型,该类型后续介绍。然后返回该节点。

如果current 存在,是组件的更新操作。 会有如下的条件来判断是否真的需要更新:

updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime &&
  (compare(prevProps, nextProps) && 
  current.ref === workInProgress.ref)

如果判断 该组件的更新时间 比 当前调度的渲染的时间要长, 说明此节点过期时间比较长还没有到达过期时间不需要马上更新,并且 compare 执行后 返回 true, ref 相同说明前后组件的 DOM 节点保持一致没有被更新。则 调用 bailoutOnAlreadyFinishedWork,否则 创建 该memo 传入的 type 函数组件对应的 workInProgress fiber 对象,并且 使用的是传入 memo 函数的 props。然后 memo 组件是该创建 fiber 节点的 父节点。返回创建的对象。

总结:对于 memo 组件 刚开始创建,current 不存在的情况下,如果是一个简单的函数组件并且compare没有的话设置为 SimpleMemoComponent 类型,该类型每一次更新的时候会对 props 进行浅对比来决定是否更新并且更新方式和 FunctionComponent 一致,也就是在 执行 FunctionComponent 之前进行 props 前对比。并且,传入的函数组件会提升,替代 memo 组件。如果 存在 compare情况下会创建 传入的 函数组件作为memo组件的孩子节点,并且child类型是 IndeterminateComponent。current 如果存在判断 memo 节点是否更新,除了比较过期时间 还增加了执行compare 回调函数。

ClassComponent

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 429

对于类组件,分为 创建、更新两个节点,挂起阶段与更新执行的一样。类组件 与 函数组件一样 对于 新的 props 回合 defaultProps 进行合并处理。
处理完成后,判断 之前是否存在该类的实例, 如果没有则说明是需要创建实例的,进入创建阶段, 否则, 如果实例存在 但是 current 不存在则说明创建了实例 但是没有进入到commit 节点,说明组件没中断或者挂起,current存在说明已经创建完成更新过了。 那么都会进入更新阶段。

创建

对于创建节点,就是 new 一个 类的实例, 然后这是类的updater、_reactInternalFiber、workInProgress 的 sateNode 指向该实例。然后将 调用 processUpdateQueue 方法更新所有的 update。方法如下面一节介绍。 更新完毕后,重新设置实例的 state 属性。再执行 getDerivedStateFromProps 方法,再一次更新 state。 接下来就是 设置 effectTag 增加 update,等待commit 阶段调用 componentDidMount。
然后进入 finishClassComponent 阶段。

更新

先执行 processUpdateQueue 方法更新所有的update,然后更新 instance 的 state 之后,有如下判断逻辑:

oldProps === newProps &&
oldState === newState &&
!hasContextChanged() &&
!hasForceUpdate

说明 props state 都没有发生变化, 并且 不是强制更新的话,则直接 设置 effectTag update、snapshot 接下来在commit 阶段能够执行 componentDidUpdate、getSnapshotBeforeUpdate 方法,并且在进入 finishClassComponent 阶段的时候传入的 shouldUpdate 是false。

否则的话,是说明要进行具体的更新过程的:

shouldUpdate =
hasForceUpdate ||
shouldComponentUpdate() ||
(
	ctor.prototype.isPureReactComponent && 
  !(shallowEqual(oldProps, newProps) && shallowEqual(oldState, newState))
)

除了设置 effectTag update、snapshot 接下来在commit 阶段能够执行 componentDidUpdate、getSnapshotBeforeUpdate 方法之外,进入 finishClassComponent 的 shouldUpdate 就是上述表达式的结果。该表达式意思为 是否有强制更新需要更新的, 或者 执行 shouldComponentUpdate 方法返回 true 否则 在 PureReactComponent 组件中 props state 前后都不相等。则意味着需要更新。

processUpdateQueue

源码位置 react/packages/react-reconciler/src/ReactUpdateQueue.js 403


该方法是处理类似于 通过 setState 回调函数的方式 产生了更新,记录在更新队列中,在渲染阶段需要处理更新队列的方法。会循环整个更新链表直到更新完成重设 workInPropgress 的 memoizedState。在每一次更新过程中,仅仅是渲染那些更新时间已经比 渲染过期时间还要短的更新,因为这些更新比较紧急。对于非紧急的更新会重新放入更新链表中作为下一个的更新。

更新每一个 updateNode的时候, 会根据更新类型执行不同的更新方法。有如下更新 replaceState、captureUpdate、updateState、forceUpdate。
对于 setState 多半是 updateState 更新:

const payload = update.payload;
let partialState;
if (typeof payload === 'function') 
  partialState = payload.call(instance, prevState, nextProps);
 else 
  partialState = payload;

if (partialState === null || partialState === undefined) 
  return prevState;

return Object.assign(, prevState, partialState);

根据传入的 setState 类型不同, 如果是函数 则 在instance 对象中 执行方法返回新的state更新,否则就是对象了,如果有新的跟新的话,使用 assign 方法合并 之前的state 和 更新后的state, 否则 还是原来的 state对象引用保持不变。
对于 forceUpdate 则直接设置 hasForceUpdate 为 true。其他的跟新不再过多的介绍。

finishClassComponent

在该方法中, 不考虑是否有异常的情况下。 如果 shouldUpdate 为 true 的话,则 调用实例的 render 方法 进入更新, 然后 进入 resoncileChildren 方法生成孩子节点, 再一次设置 state。否则 调用 bailoutOnAlreadyFinishedWork。

IndeterminateComponent


在 memo 函数创建过程中, 如果 current 为空的话 ,存在 compare 方法,会创建传入的函数对象,此时的函数对象的type 就是IndeterminateComponent,这个类型可以定义成 类似的 类类型。只要执行 函数方法后,得到的如果存在 render 方法 那么就会将 节点的type 设置成 ClassComponent 否则会设置成 FunctionComponent 然后进入后续的操作。

reconcileChildren

源码位置 react/packages/react-reconciler/src/ReactFiberBeginWork.js 128

处理完一个节点后,会产生 child,调用该方法将会产生创建或者更新操作,由于 react 每一次的更新都是 跟新的 树与前一课 树对比 收集更新的过程, 上述 beginWork 中对于每一种类型都是处理每一个节点上的具体的更新已经执行响应的更新动作收集此节点的更新动作。 但是跟新后对于孩子节点会产生变化, 比如 数组节点会产生顺序、增删改查的变化,单节点会产生类型的变化, 那么对于孩子节点的增删改查变化的收集 需要此方法完成。产生child 更新后,产生的child 可能存在如下类型 FRAGMENT、单节点react对象、数组、字符串数字、null等,主要介绍这几种常用的更新。
在执行完毕后,会将新产生的节点 会通过 return sibling 互相连接起来。具体 react 优化措施 在 React Diff 优化算法 中介绍。

REACT_FRAGMENT_TYPE

对于 返回的是 fragment 类型的节点,直接返回其孩子节点进入该方法再一次判断 处理孩子节点,相当于忽略 fragment 节点的包裹。

REACT_ELEMENT_TYPE

对于 更新后返回来单个 react 节点,会在 current 中查找与更新后节点type 和 key 都一致的 节点,如果存在则复用该 fiber 节点作为 workInProgress 的节点,删除剩下的其他的没用用到的节点, 这里的删除并不是马上删除掉,而是在父节点的effect 链表中添加删除命令等到真正的 commit 的时候再去处理这些变动。如果都没有找到则需要去创建新的workInProgress 节点,此时旧的所有的current 的孩子节点都将标记 deletion。
创建的时候 创建的节点的type 是 使用 createFiberFromTypeAndProps, 所以对于 函数方法的孩子节点则会是 IndeterminateComponent 类型会进入到 bebginWork 的 IndeterminateComponent 阶段 进一步的判断是 class 还是 function。

Array

对于更新后返回的是一个数组类型来说,react 认为 更新 会比 增加删除更加频繁, 所以该方法首先处理的是更新的操作。然后是 增加,删除。

对于更新来说,如果数组内更新前后顺序 保持一致, 并且 key、type 也是一致的话,则将复用。 如果有新增的节点直接创建。比较有意思的一点是,对于修改位置、删除元素、修改元素类型来说,都会从旧的 节点中根据 key和type 找到可以复用的节点,没有则直接创建。不过是将旧的节点生成map的方式寻找的效率比较慢, 所以尽可能的保证 顺序 key type 不变对于数组的更新来说是必不可少的。

string/number

如果current 树对应的节点也是 text 类型的话 复用,否则重新创建 workInProgress 的 text 节点。

null/undefined 等其他情况

对于这种情况,说明变化后,孩子节点设置为空了,则需要执行清除current 对应的所有孩子节点的操作, 将想这些 删除的 effect 设置到该父节点effect 链表中等待 commit 处理这些副作用。

总结:reconcileChildren 方法处理完孩子节点后并且收集了更新依赖添加到父节点的effect 链表中后,返回 child 进入 beginWork 继续进行循环处理。

completeUnitOfWork


等到 reconcileChildren 处理完毕 child 后 返回的 child 是 null。 或者 beginWork 中 没有更新 执行 bailoutOnAlreadyFinishedWork 返回的也是 null。则需要进入接下来的 completeUnitOfWork 阶段执行, 该阶段主要是对处理完成的一条通路进行收集 Effect,重新设置节点的过期时间,然后收集 DOM 变更。该方法收集了一个节点后判断 兄弟节点是否存在如果存在则需要进一步的访问处理, 否则就继续向上访问父节点继续effect收集等过程。completeWork 是进行 DOM 变更收集的过程,也是进行 DOM diff 的过程, 过程如下:

completeWork

这里主要介绍 hostComponent, 对于其他的比较简单不做介绍了因为 其他的比如 classComponent类型仅仅是react 为了封装 原生节点比如 div 之类的产生的不会进入 commit 阶段进行更新。 对于第一次创建更新的时候,也就是 current 或者 stateNode 不存在的时候,则会直接创建 DOM 节点, 然后 将 DOM 节点 append 到父节点中,注意的是, 这里的 DOM 节点是将贴近父节点的一层的孩子节点, 因为 在 completeUnitOfWork 中 是逐步回溯的,被创建的孩子节点其实已经收集了它的孩子节点的 dom 节点了, 所以只要收集附近的一层孩子节点就可以。stateNode 不存在比如该节点是新创建或者更新的,则 stateNode 就不会存在。
对于都存在的节点可以直接复用 dom 节点, 但是需要更新该节点上的一些属性,比如 样式等。然后最终都会添加一个 update Effect。


React Diff 算法优化

react 的 虚拟 Diff 比较算法 算法 有几个预设的条件:

  • 只对同级元素进行 diff, 如果一个 DOM 节点在前后的两次更新中跨越了层级, 那么 React 不会尝试复用他。
  • 两个不同类型的元素会产生出不同的树, 如果元素由 div 变成 p,那么 销毁 div以及子孙节点,并新建 p 以及 子孙节点。
  • 开发者可以通过 key 属性来暗示那些子元素在不同的渲染下能保持稳定。

具体的 虚拟 DOM 对比就是 上面介绍的 **reconcileChildren **方法,该方法除了可以创建更新孩子节点,将 reactNode 转变成 fiberNode外,为了优化更新,内部逻辑增加了 根据 key type 顺序等进行了优化。在这个方法中分为了两类更新,一类是单节点的更新:比如 object、string、number。另一类的多节点的更新比如 array 等。

第一种情况:同级别只有一个节点的 diff

我们以 object 类型 type 是 REACT_ELEMENT_TYPE 的 节点更新为例, 代码大概如下:

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  expirationTime: ExpirationTime,
): Fiber 
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) 
    // 判断 key 是否相同,注意 如果 key 为 null 也算是相同
    if (child.key === key) 
      // 如果 key 相同,并且 type 元素都相同
      if (child.elementType === element.type) 
        // 剩下的节点标记为删除
        deleteRemainingChildren(returnFiber, child.sibling);
        // 复用该节点,并且返回该节点
        const existing = useFiber( child, element.props, expirationTime);
        existing.return = returnFiber;
        return existing;
       else 
        // 元素以及剩下的元素都标记为删除,并且结束
        deleteRemainingChildren(returnFiber, child);
        break;
      
     else 
      deleteChild(returnFiber, child);
    
    child = child.sibling;
  

  // 创建新的 fiber, 并且返回

意思就是从current fiber 中找到 key 相同并且 type 也相同的元素复用,否则 创建新的元素并且将current 中所有的元素标记为删除。所以 对于单一节点的更新 满足了 react 预设的二三条件, type 相同 并且 key 也相同。比如局如下例子:

// 例1: 更新前
<p>123</p>
更新后
<p>1234</p>

// 例2: 更新前
<div key="key1">123</div>
// 更新后
<p key="key12">1234</p>

// 例3: 更新前
<p key="key1">123</p>
<p key="key2">456</p>
// 更新后
<p key="key1">1234</p>

其中:
例1: 是不需要重新创建的, 因为 type都是 p hostComponent类型,并且 key 都是 null。只不过children 变化了。
例2:需要更新,因为 type 类型虽然都是 hostComponent,但是 type 值从 div 变成了 p,同时key 也发生了改变。
例3:不需要更新可以复用,因为虽然更新前存

以上是关于4. render的主要内容,如果未能解决你的问题,请参考以下文章

ReactDOM.render

Parcel JS:tree.render 不是函数

react 生命周期执行顺序,render执行条件

render函数

Real-Time Rendering-第四章 Transforms

mvc中Scripts.RenderStyles.Render