ReactDOM.render 是如何串联渲染链路的?(中)

ReactDOM.render 是如何串联渲染链路的?(中)

  render 阶段可以认为是整个渲染链路中最为核心的一环,因为反复强调“找不同”的过程,恰恰就是在这个阶段发生的。render阶段做的事情有很多,以 beginWork 为线索,着重探讨 Fiber树的构建过程。

拆解 ReactDOM.render 调用栈——render 阶段

  首先,复习一下 render阶段在整个渲染链路中的定位,如下图所示。

在这里插入图片描述

  图中,performSyncWorkOnRoot标志着 render阶段的开始,finishSyncRender标志着 render阶段的结束。这中间包含了大量的 beginWorkcompleteWork调用栈,正是 render的工作内容。

beginWorkcompleteWork这两个方法需要注意,它们串联起的是一个“模拟递归”的过程。

  调和过程是一个递归的过程。而 Fiber架构下的调和过程,虽然并不是依赖递归来实现的,但在 ReactDOM.render 触发的同步模式下,它仍然是一个深度优先搜索的过程。在这个过程中,beginWork将创建新的 Fiber 节点,而 completeWork则负责将 Fiber 节点映射为 DOM 节点。

  那么问题就来了:我们的 Fiber树都还长这个样子:

在这里插入图片描述

  就这么个样子,遍历它,能遍历出来什么?到底怎么个遍历法?接下来就深入到源码里去一探究竟!

workInProgress 节点的创建

  erformSyncWorkOnRootrender阶段的起点,而这个函数最关键的地方在于它调用了 renderRootSync。下面放大 Performance调用栈,来看看 renderRootSync被调用后,紧接着发生了什么:

在这里插入图片描述

  紧随其后的是 prepareFreshStack,这里不卖关子,prepareFreshStack的作用是重置一个新的堆栈环境,其中最需要我们关注的步骤,就是对createWorkInProgress 的调用。以下我对 createWorkInProgress的主要逻辑进行了提取(解析在注释里):

// 这里入参中的 current 传入的是现有树结构中的 rootFiber 对象
function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;
  // ReactDOM.render 触发的首屏渲染将进入这个逻辑
  if (workInProgress === null) {
    // 这是需要你关注的第一个点,workInProgress 是 createFiber 方法的返回值
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // 这是需要你关注的第二个点,workInProgress 的 alternate 将指向 current
    workInProgress.alternate = current;
    // 这是需要你关注的第三个点,current 的 alternate 将反过来指向 workInProgress
    current.alternate = workInProgress;
  } else {
    // else 的逻辑此处先不用关注
  }
  // 以下省略大量 workInProgress 对象的属性处理逻辑
  // 返回 workInProgress 节点
  return workInProgress;
}

  首先要声明的是,该函数中的 current 入参指的是现有树结构中的 rootFiber 对象,如下图所示:

在这里插入图片描述

  源码重点如下:

  • createWorkInProgress调用 createFiberworkInProgress是 createFiber 方法的返回值
  • workInProgressalternate 将指向 current
  • current 的 alternate 将反过来指向 workInProgress

  下面看看 createFiber的逻辑:

var createFiber = function (tag, pendingProps, key, mode) {
  return new FiberNode(tag, pendingProps, key, mode);
};

  代码出奇的简单,但信息却给得很到位 —— createFiber 将创建一个 FiberNode 实例,而 FiberNode,上一讲已经讲过,它正是 Fiber节点的类型。因此 workInProgress 就是一个 Fiber 节点。不仅如此,细心的你可能还会发现 workInProgress的创建入参其实来源于 current,如下面代码所示:

 workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);

  实锤了,workInProgress 节点其实就是 current 节点(即 rootFiber)的副本

  再结合 current指向 rootFiber对象(同样是 FiberNode实例),以及 currentworkInProgress通过 alternate 互相连接这些信息,我们可以分析出这波操作执行完之后,整棵树的结构应该如下图所示:

在这里插入图片描述

  完成了这个任务之后,就会进入 workLoopSync的逻辑。这个 workLoopSync函数也是个“人狠话不多”的主,它的逻辑同样是简洁明了的,如下所示(解析在注释里):

function workLoopSync() {
  // 若 workInProgress 不为空
  while (workInProgress !== null) {
    // 针对它执行 performUnitOfWork 方法
    performUnitOfWork(workInProgress);
  }
}

  workLoopSync 做的事情就是通过 while 循环反复判断 workInProgress 是否为空,并在不为空的情况下针对它执行 performUnitOfWork 函数

  而 performUnitOfWork函数将触发对 beginWork 的调用,进而实现对新 Fiber 节点的创建。若 beginWork所创建的 Fiber节点不为空,则 performUniOfWork会用这个新的 Fiber节点来更新 workInProgress的值,为下一次循环做准备

  通过循环调用 performUnitOfWork 来触发 beginWork,新的 Fiber 节点就会被不断地创建。当 workInProgress终于为空时,说明没有新的节点可以创建了,也就意味着已经完成对整棵 Fiber 树的构建。

  在这个过程中,每一个被创建出来的新 Fiber 节点,都会一个一个挂载为最初那个 workInProgress 节点(如下图高亮处)的后代节点。而上述过程中构建出的这棵 Fiber 树,也正是大名鼎鼎的 workInProgress 树

在这里插入图片描述

  相应地,图中 current指针所指向的根节点所在的那棵树,我们叫它“current 树”。

  **问题来了:**一棵 current树,一棵 workInProgress树,这名堂也太多了吧!况且这两棵 Fiber 树至少在现在看来,是完全没区别的(毕竟都还只有一个根节点,哈哈)。React 这样设计的目的何在?或者换个问法——到底是什么样的事情一棵树做不到,非得搞两棵“一样”的树出来?

  接下来深入到 beginWorkcompleteWork的逻辑里去,一起看看 Fiber 树的构建过程及最终形态。

beginWork 开启 Fiber 节点创建过程

  beginWork的源码实在是长到不科学,针对与树构建过程强相关的动作进行逻辑提取,代码如下(解析在注释里):

function beginWork(current, workInProgress, renderLanes) {

  ......

  //  current 节点不为空的情况下,会加一道辨识,看看是否有更新逻辑要处理
  if (current !== null) {
    // 获取新旧 props
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;
    // 若 props 更新或者上下文改变,则认为需要"接受更新"
    if (oldProps !== newProps || hasContextChanged() || (
     workInProgress.type !== current.type )) {
      // 打个更新标
      didReceiveUpdate = true;
    } else if (xxx) {
      // 不需要更新的情况 A
      return A
    } else {
      if (需要更新的情况 B) {
        didReceiveUpdate = true;
      } else {
        // 不需要更新的其他情况,这里我们的首次渲染就将执行到这一行的逻辑
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  } 

  ......

  // 这坨 switch 是 beginWork 中的核心逻辑,原有的代码量相当大
  switch (workInProgress.tag) {

    ......

    // 这里省略掉大量形如"case: xxx"的逻辑
    // 根节点将进入这个逻辑
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes)
    // dom 标签对应的节点将进入这个逻辑
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes)
    // 文本节点将进入这个逻辑
    case HostText:
      return updateHostText(current, workInProgress)

    ...... 

    // 这里省略掉大量形如"case: xxx"的逻辑
  }

  // 这里是错误兜底,处理 switch 匹配不上的情况
  {
    {
      throw Error(
        "Unknown unit of work tag (" +
          workInProgress.tag +
          "). This error is likely caused by a bug in React. Please file an issue."
      )
    }
  }
}

  beginWork 源码重点总结:

  1. beginWork的入参是一对用 alternate 连接起来的 workInProgress 和 current 节点
  2. beginWork 的核心逻辑是根据 fiber 节点(workInProgress的 tag 属性的不同,调用不同的节点创建函数

  当前的 current节点是 rootFiber,而 workInProgress则是 current的副本,它们的 tag 都是 3,如下图所示:

  这里先不必急于关注 updateHostRoot的逻辑细节。事实上,在整段 switch逻辑里,包含的形如“update+类型名”这样的函数是非常多的。在示例的 Demo 中,就涉及了对 updateHostRootupdateHostComponent等的调用,十来种 updateXXX,我们不可能一个一个去扣每一个函数的逻辑。

  幸运的是,这些函数之间不仅命名形式一致,工作内容也相似。就 render 链路来说,它们共同的特性,就是都会通过调用 reconcileChildren 方法,生成当前节点的子节点

reconcileChildren的源码如下:

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  // 判断 current 是否为 null
  if (current === null) {
    // 若 current 为 null,则进入 mountChildFibers 的逻辑
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // 若 current 不为 null,则进入 reconcileChildFibers 的逻辑
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

  从源码来看,reconcileChildren也只是做逻辑的分发,具体的工作还要到 mountChildFibersreconcileChildFibers 里去看。

ChildReconciler,处理 Fiber 节点的幕后“操盘手”

  reconcileChildFibersmountChildFibers不仅名字相似,出处也一致。它们都是 ChildReconciler 这个函数的返回值,仅仅存在入参上的区别。而 ChildReconciler,则是一个实打实的“庞然大物”,其内部的逻辑量堪比 N 个 beginWork。这里将关键要素提取如下(解析在注释里):

function ChildReconciler(shouldTrackSideEffects) {
  // 删除节点的逻辑
  function deleteChild(returnFiber, childToDelete) {
    if (!shouldTrackSideEffects) {

      // Noop.

      return;
    } 

    // 以下执行删除逻辑

  }

  ......

  // 单个节点的插入逻辑
  function placeSingleChild(newFiber) {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags = Placement;
    }
    return newFiber;
  }

  // 插入节点的逻辑
  function placeChild(newFiber, lastPlacedIndex, newIndex) {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {

      // Noop.

      return lastPlacedIndex;
    }

    // 以下执行插入逻辑

  }

  ......

  // 此处省略一系列 updateXXX 的函数,它们用于处理 Fiber 节点的更新

  // 处理不止一个子节点的情况
  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
    ......
  }
  // 此处省略一堆 reconcileXXXXX 形式的函数,它们负责处理具体的 reconcile 逻辑
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
      
    // 这是一个逻辑分发器,它读取入参后,会经过一系列的条件判断,调用上方所定义的负责具体节点操作的函数
      
  }
  // 将总的 reconcileChildFibers 函数返回
  return reconcileChildFibers;
}

  针对与主流程强相关的逻辑总结以下要点:

  1. 关键的入参 shouldTrackSideEffects,意为“是否需要追踪副作用”,因此 reconcileChildFibers 和 mountChildFibers 的不同,在于对副作用的处理不同
  2. ChildReconciler中定义了大量如 placeXXXdeleteXXXupdateXXXreconcileXXX等这样的函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被 reconcileChildFibers所调用;
  3. ChildReconciler的返回值是一个名为 reconcileChildFibers的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的 Fiber 节点操作,最终返回不同的目标 Fiber 节点

  对副作用的处理不同,到底是哪里不同?以 placeSingleChild为例,以下是 placeSingleChild的源码:

function placeSingleChild(newFiber) {
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags = Placement;
  }
  return newFiber;
}

  可以看出,一旦判断 shouldTrackSideEffectsfalse,那么下面所有的逻辑都不执行了,直接返回。那如果执行下去会发生什么呢?简而言之就是给 Fiber节点打上一个叫“flags”的标记,像这样:

newFiber.flags = Placement;
小科普:flags 是什么

  Placement 这个 flags的意义,是在渲染器执行时,也就是真实 DOM 渲染时,告诉渲染器:我这里需要新增 DOM 节点flags记录的是副作用的类型,而所谓“副作用”,React 给出的定义是“数据获取、订阅或者修改 DOM”等动作。在这里,Placement 对应的显然是 DOM 相关的副作用操作。

  回到我们的调用链路里来,由于 currentrootFiber,它不为 null,因此它将走入的是下图所高亮的这行逻辑。也就是说在 mountChildFibersreconcileChildFibers之间,它选择的是 reconcileChildFibers

在这里插入图片描述

  结合前面的分析可知,reconcileChildFibersChildReconciler(true)的返回值。入参为 true,意味着其内部逻辑是允许追踪副作用的。

  接下来进入 reconcileChildFibers的逻辑,在 reconcileChildFibers这个逻辑分发器中,会把 rootFiber子节点的创建工作分发给 reconcileXXX函数家族的一员——reconcileSingleElement来处理,具体的调用形式如下图高亮处所示:

在这里插入图片描述

  reconcileSingleElement将基于 rootFiber子节点的 ReactElement对象信息,创建其对应的 FiberNode。这个过程中涉及的函数调用如下图高亮处所示:

在这里插入图片描述

  rootFiber 作为 Fiber 树的根节点,它并没有一个确切的 ReactElement与之映射。结合 JSX 结构来看,我们可以将其理解为是 JSX 中根组件的父节点。给出的 Demo 中,组件编码如下:

import React from "react";
import ReactDOM from "react-dom";
function App() {
    return (
      <div className="App">
        <div className="container">
          <h1>我是标题</h1>
          <p>我是第一段话</p>
          <p>我是第二段话</p>
        </div>
      </div>
    );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

  可以看出,根组件是一个类型为 App 的函数组件,因此 rootFiber 就是 App 的父节点

  结合这个分析来看,图中的 _created4是根据 rootFiber的第一个子节点对应的 ReactElement来创建的 Fiber节点,那么它就是 App 所对应的 Fiber 节点。现在我为你打印出运行时的 _created4值,会发现确实如此:

在这里插入图片描述

  App 所对应的 Fiber 节点,将被 placeSingleChild打上“Placement”(新增)的副作用标记,而后作为 reconcileChildFibers函数的返回值,返回给下图中的 workInProgress.child

在这里插入图片描述

  reconcileChildren函数上下文里的 workInProgress就是 rootFiber节点。那么此时,我们就将新创建的 App Fiber 节点和 rootFiber关联了起来,整个 Fiber 树如下图所示:

在这里插入图片描述

Fiber 节点的创建过程梳理

  分析完 App FiberNode 的创建过程,先不必急于继续往下走这个渲染链路。因为其实最关键的东西已经讲完了,剩余节点的创建只不过是对 performUnitOfWorkbeginWorkChildReconciler等相关逻辑的重复。

  为了方便把握逻辑脉络,将beginWork所触发的调用流程总结进一张大图:

在这里插入图片描述

Fiber 树的构建过程

  理解了 Fiber 节点的创建过程,就不难理解 Fiber 树的构建过程了。

循环创建新的 Fiber 节点

  研究节点创建的工作流,切入点是workLoopSync这个函数。为什么选它?这里来复习一遍workLoopSync会做什么:

function workLoopSync() {
  // 若 workInProgress 不为空
  while (workInProgress !== null) {
    // 针对它执行 performUnitOfWork 方法
    performUnitOfWork(workInProgress);
  }
}

  它会循环地调用 performUnitOfWork,而 performUnitOfWork,开篇我们已经点到过它,其主要工作是“通过调用 beginWork,来实现新 Fiber节点的创建”;它还有一个次要工作,就是把新创建的这个 Fiber 节点的值更新到 workInProgress 变量里去。源码中的相关逻辑提取如下:

// 新建 Fiber 节点
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
// 将新的 Fiber 节点赋值给 workInProgress
if (next === null) {
  // If this doesn't spawn new work, complete the current work.
  completeUnitOfWork(unitOfWork);
} else {
  workInProgress = next;
}

  如此便能够确保每次 performUnitOfWork执行完毕后,当前的 workInProgress 都存储着下一个需要被处理的节点,从而为下一次的 workLoopSync 循环做好准备

  在 workLoopSync内部打个断点,尝试输出每一次获取到的 workInProgress的值,workInProgress值的变化过程如下图所示:

在这里插入图片描述

  共有 7 个节点,若点击展开查看每个节点的内容,就会发现这 7 个节点其实分别是:

  • rootFiber(当前 Fiber 树的根节点)

  • App FiberNode(App 函数组件对应的节点)

  • class为 App 的 DOM 元素对应的节点,其内容如下图所示

    在这里插入图片描述

  • classcontainer的 DOM 元素对应的节点,其内容如下图所示

    在这里插入图片描述

  • h1 标签对应的节点

  • 第 1 个 p 标签对应的 FiberNode,内容为“我是第一段话”,如下图所示

    在这里插入图片描述

  • 第 2 个 p 标签对应的 FiberNode,内容为“我是第二段话”,如下图所示

    在这里插入图片描述

  结合7个FiberNode,对照demo,发现组件自上而下,每一个非文本类型的 ReactElement 都有了它对应的 Fiber 节点

  这样一来,构建的这棵树里,就多出了不少 FiberNode,如下图所示:

在这里插入图片描述

Fiber 节点间是如何连接的呢

  不同的 Fiber 节点之间,将通过 child、return、sibling 这 3 个属性建立关系其中 child、return 记录的是父子节点关系,而 sibling 记录的则是兄弟节点关系

  这里以 h1 这个元素对应的 Fiber 节点为例,给你展示下它是如何与其他节点相连接的。展开这个 Fiber节点,对它的 childreturnsibling3 个属性作截取,如下图所示:

  child 属性为 null,说明 h1节点没有子 Fiber节点:

在这里插入图片描述

return属性局部截图:

在这里插入图片描述

sibling属性局部截图:

在这里插入图片描述

  可以看到,return属性指向的是 classcontainerdiv节点,而 sibling属性指向的是第 1 个 p 节点。结合 JSX 中的嵌套关系我们不难得知 ——FiberNode 实例中,return 指向的是当前 Fiber 节点的父节点,而 sibling 指向的是当前节点的第 1 个兄弟节点

  结合这 3 个属性所记录的节点间关系信息,将上面梳理出来的新 FiberNode 连接起来:

在这里插入图片描述

  以上便是 workInProgressFiber 树的最终形态了。从图中可以看出,虽然人们习惯上仍然将眼前的这个产物称为“Fiber 树”,但它的数据结构本质其实已经从树变成了链表

  在分析 Fiber 树的构建过程时,我们选取了 beginWork 作为切入点,但整个 Fiber 树的构建过程中,并不是只有 beginWork在工作。这其中,还穿插着 completeWork 的工作。只有将 completeWorkbeginWork放在一起来看,才能够真正理解,Fiber 架构下的“深度优先遍历”到底是怎么一回事。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值