美文网首页
ReactDOM.render 串联渲染链路中的 beginWo

ReactDOM.render 串联渲染链路中的 beginWo

作者: 弱冠而不立 | 来源:发表于2021-01-28 10:25 被阅读0次

    参考文章:React技术揭秘——beginWork
    需要先看:
    React 中的双缓冲机制
    ReactDOM.render 串联渲染链路 —— render 阶段“递归”概览
    截止到初始化阶段的时候,我们的 Fiber 树现在仅有一个根节点 rootFiber

    fiberRoot 的关联对象是真实 DOM 的容器节点,rootFiber(也就是 fiberRoot 的 current 属性) 则作为虚拟 DOM 的根节点存在

    所以首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用 beginWork方法
    由于整个 beginWork 模块代码量太多,先进行概览然后分几步解读

    逻辑概览(react 版本 v17.0.1)

    function beginWork(
      current: Fiber | null,    // 当前组件在页面渲染的Fiber节点
      workInProgress: Fiber,    // 内存中构建的 workInProgress Fiber 树的 
      renderLanes: Lanes      // 优先级相关,和 Scheduler 有关
    ): Fiber | null {
    
      // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
      if (current !== null) {
        // ...省略
    
        // 复用current
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      } else {
        didReceiveUpdate = false;
      }
    
      // mount时:根据tag不同,创建不同的子Fiber节点
      switch (workInProgress.tag) {
        case IndeterminateComponent: 
          // ...省略
        case LazyComponent: 
          // ...省略
        case FunctionComponent: 
          // ...省略
        case ClassComponent: 
          // ...省略
        case HostRoot:
          // ...省略
        case HostComponent:
          // ...省略
        case HostText:
          // ...省略
        // ...省略其他类型
      }
    }
    

    从传参看

    function beginWork(
      current: Fiber | null,     // 当前组件在页面渲染的Fiber节点
      workInProgress: Fiber,    // 内存中构建的 workInProgress Fiber 树的 Fiber节点
      renderLanes: Lanes,    // 优先级相关,和 Scheduler 有关
    ): Fiber | null {
      // ...省略函数体
    }
    

    传参详情:

    • current:当前组件在页面对应的Fiber节点,也是 workInProgress.alternate
    • workInProgress: 当前组件在内存中构建的Fiber节点,也是 current.alternate
    • renderLanes:优先级相关,与 Scheduler 有关

    React 中的双缓冲机制有介绍:
    在mount阶段,由于首次渲染,整个 Fiber 树只有一个 rootFiber(rootFiber 对应的是虚拟DOM的根节点和实际DOM中的root节点对应)。不存在当前组件对应的 Fiber 节点(如 App 节点此时也是不存在的) 。即 mount 时 current === null.
    而由于组件要 update时,由于之前已经 mount 过了,整个 Fiber 树已经渲染到页面上去了,所以 current !== null

    基于此原因,beginWork 的工作可以分为两部分:

    • mount 时:除了 rootFiber (fiberRootNode的实例) 以外,组件的Fiber节点都不存在,即 current === null。会根据 fiber.tag 不同,创建不同类型的子Fiber节点。
    • update 时:current !== null,这个时候 workInProgress Fiber 树就可以考虑复用 current 节点。

    update 时

    进入 current !== null 还有一个有关didReceiveUpdate是否为true的逻辑(为 false 则节点可复用,为 ture 不可复用):

    1. workInProgress.type !== current.type (fiber.type变了,不可复用)
    2. oldProps !== newProps(props改变,不可复用)
    3. hasContextChanged() 返回值为 true (看名字估计是 Context 有变也不可复用)
    4. !includesSomeLane(renderLanes, updateLanes) 为 false(当前节点优先级不够,不可复用)
    5. (current.flags & ForceUpdateForLegacySuspense) !== NoFlags 为真,不可复用(看源码注释这个判断是后来为了修复一些好像是只存在于 legacy mode的特例情况,有关此代码的详情可以戳这里
      :legacy mode 就是当前使用的 ReactDOM.render 启动的模式,也就是所谓的遗留模式

    源码逻辑概览:

      if (current !== null) {
        var oldProps = current.memoizedProps;
        var newProps = workInProgress.pendingProps;
    
        if (oldProps !== newProps || hasContextChanged() || ( 
         workInProgress.type !== current.type )) {
          didReceiveUpdate = true;
        } else if (!includesSomeLane(renderLanes, updateLanes)) {
          didReceiveUpdate = false; 
    
          switch (workInProgress.tag) {
              //....一堆case
          }
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        } else {
          if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
            didReceiveUpdate = true;
          } else {
            didReceiveUpdate = false;
          }
        }
      } else {
        didReceiveUpdate = false;
      }
    

    mount 时

    不满足上一路径时,我们就进入第二部分,新建Fiber。
    我们可以看到,根据fiber.tag不同,进入不同类型Fiber的创建逻辑。

    可以从这里看到tag对应的组件类型

    // mount时:根据tag不同,创建不同的Fiber节点
    switch (workInProgress.tag) {
      case IndeterminateComponent: 
        // ...省略
      case LazyComponent: 
        // ...省略
      case FunctionComponent: 
        // ...省略
      case ClassComponent: 
        // ...省略
      case HostRoot:
        // ...省略
      case HostComponent:
        // ...省略
      case HostText:
        // ...省略
      // ...省略其他类型
    }
    

    对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法。

    reconcileChildren

    这个方法里面代码很简单:

    function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
      if (current === null) {
        // current为空,即处于mount阶段,
        //先在内存中构建 workInProgress Fiber 树
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
      } else {
        // current 不为空,即处于update阶段,
        // 同样也先构建 workInProgress Fiber 树,但此时可以考虑复用 rootFiber 下的 Fiber树节点
        workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
      }
    }
    

    代码上看,也是通过 current === null ?区分 mountupdate.

    不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress传参

    简单翻译一下就是,workInProgress 得到 Fiber 子节点后,继续从该子 Fiber 节点往下生成 workInProgress Fiber 树.

    :值得一提的是,mountChildFibersreconcileChildFibers 这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers 会为生成的 Fiber 节点带上effectTag 属性,而 mountChildFibers 不会。同时 reconcileChildFibers 会考虑是否需要复用节点,此时就利用到了 Diffing 算法。

    effectTag

    render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的 DOM 操作。要执行 DOM 操作的具体类型就保存在fiber.effectTag中。

    你可以从这里看到effectTag对应的DOM操作

    简单举几个例子:

    // DOM需要插入到页面中
    export const Placement = /*                */ 0b00000000000010;
    // DOM需要更新
    export const Update = /*                   */ 0b00000000000100;
    // DOM需要插入到页面中并更新
    export const PlacementAndUpdate = /*       */ 0b00000000000110;
    // DOM需要删除
    export const Deletion = /*                 */ 0b00000000001000;
    

    通过二进制表示 effectTag,可以方便的使用位运算为fiber.effectTag赋值多个effect

    这个时候有个问题,上面刚刚说过了,mount 时,reconcileChildren 调用了 mountChildFibers 且这个方法不会为 Fiber 节点赋值effectTag。那首屏渲染时如何将未有的节点插入进页面的呢?

    假设 mountChildFibers 也会赋值 effectTag,那么可以预见 mount 时整棵 Fiber 树所有节点的 effectTag 值都为 Placement。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的 DOM 操作是极低效的。但是,别忘了我们还有一个 rootFiber,所以在 mount 时,只要给 rootFiber 的 effectTag 赋值 Placement,在 commit 阶段只会执行一次插入操作。

    相关文章

      网友评论

          本文标题:ReactDOM.render 串联渲染链路中的 beginWo

          本文链接:https://www.haomeiwen.com/subject/vdhnzktx.html