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

2021-01-28  本文已影响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 {
  // ...省略函数体
}

传参详情:


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

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

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 阶段只会执行一次插入操作。

上一篇下一篇

猜你喜欢

热点阅读