react-reconcilation(render)阶段概览

loading 2024年01月17日 103次浏览

1. performUnitOfWork

render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

两种方法区别在于是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。

workInProgress代表当前已创建的workInProgress fiber。

performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

而源码实现中所谓的“递归”阶段就体现在performUnitOfWork方法中,也就是beginWorkcompleteWork

这里的递归很好理解,可以参考另一篇博客中的讲解,已经比较清晰了,实际上就是深度优先遍历。

考虑到性能优化,文本叶子节点不参与遍历。

2. beginWork

一句话总结,beginWork的工作是传入当前Fiber节点,创建子Fiber节点。也就是构建wip fiber tree。

mount时会直接创建,update时会根据diff算法决定是否复用再创建。具体看2.3小节

先看看传入的参数:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}
  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate,也就是current tree上的节点。
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级(Scheduler)相关

我们可以通过current是否等于null来确定当前是mount阶段还是update阶段。因为mount阶段是没有current tree的。

因此我们就可以根据这个判断条件,来区分开beginWork在mount和update时的不同内容:

2.1 mount

fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点。

所以mount时,代码长这样:

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

2.2 update

如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。

这里也就用到了diff算法来决定复不复用。

满足以下两种情况时,wip tree直接复用current tree的节点(didReceiveUpdate === false):

  • oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变。
  • !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够。

代码长这样:

if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // 省略处理
      }

      // 复用current
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }

2.3 reconcileChildren

上面提到新建子节点的时候会根据不同的tag来决定调用什么方法,对于我们常见的组件类型,如FunctionComponent/ClassComponent/HostComponent,最终就会进入这个reconcileChildren方法。

  • 对于mount的组件,他会创建新的子Fiber节点

  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

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

2.4 总结

最终来看看beginWork的工作流程图:

3. completeWork

组件执行beginWork后会创建子Fiber节点,节点上可能存在effectTag(update的情况下)。那completeWork会做什么工作呢?

先看看代码:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

首先入参和beginWork是差不多的,就不展开说明;其次,可以看出来completeWork也是根据不同fiber.tag调用不同的处理逻辑。

3.1 HostComponent

先来看看页面渲染所必须的HostComponent

HostComponent指的是对应于DOM元素或者原生组件的React组件,比如div。

同样的,也得根据current是否为null来判断当前是mount阶段还是update阶段。

3.1.1 mount

mount时的主要逻辑包括三个:

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程

注意,这里只是将DOM节点创建出来放在内存中,commit阶段才会将这个DOM节点插入到真正的DOM树中。

// ...省略服务端渲染相关逻辑

const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?

原因就在于上面代码的appendAllChildren方法。

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树,直接整棵树插入页面即可。

3.1.2 update

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClick、onChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop

最主要的逻辑是调用updateHostComponent方法。

if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
}

updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

3.2 effectList

经过了上面的流程,render阶段的绝大部分工作就完成了。

还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?

这显然是很低效的。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect

类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

这样,在commit阶段只需要遍历effectList就能执行所有effect了。

借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。

3.3 总结

completeWork工作流程图

至此,render阶段全部工作完成。在performSyncWorkOnRoot或是performConcurrentWorkOnRoot函数中,fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。