React render 阶段的执行过程

发布于 2023-11-22 20:23:04 字数 12620 浏览 41 评论 0

关键词:react16 架构、react Reconciler、react fiber、react 协调器

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 的工作可以分为两部分:“递”和“归”。

创建节点

首先从 rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用 beginWork 方法 (opens new window)。

该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

在“归”阶段会调用 completeWork (opens new window)处理 Fiber 节点。

当某个 Fiber 节点执行完 completeWork,如果其存在兄弟 Fiber 节点(即 fiber.sibling !== null),会进入其兄弟 Fiber 的递阶段。

如果不存在兄弟 Fiber,会进入父级 Fiber 的归阶段。

递和归阶段会交错执行直到“归”到 rootFiber。至此,render 阶段的工作就结束了。

举例

代码如下:

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App/>, document.getElementById("root"));

对应的 fiber 树结构如下

render 阶段会依次执行

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

beginWork

源码链接: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js

工作流程图:

beginWork 的工作是传入当前 Fiber 节点,创建子 Fiber 节点,我们从传参来看看具体是如何做的。

传参

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

beginWork 的工作可以分为两部分:

  • update 时:如果 current 存在,在满足一定条件时可以复用 current 节点,这样就能克隆 current.child 作为 workInProgress.child,而不需要新建 workInProgress.child。
  • mount 时:除 fiberRootNode 以外,current === null。会根据 fiber.tag 不同,创建不同类型的子 Fiber 节点
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): 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:
    // ...省略
    // ...省略其他类型
  }
}

update 时

满足如下情况时 didReceiveUpdate === false(即可以直接复用前一次更新的子 Fiber,不需要新建子 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) {
      // 省略处理
    }
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }
} else {
  didReceiveUpdate = false;
}
  1. oldProps === newProps && workInProgress.type === current.type ,即 props 与 fiber.type 不变
  2. !includesSomeLane(renderLanes, updateLanes) ,即当前 Fiber 节点优先级不够,会在讲解 Scheduler 时介绍

mount

当不满足优化路径时,我们就进入第二部分,新建子 Fiber。

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

我们可以看到,根据 fiber.tag 不同,进入不同类型 Fiber 的创建逻辑。

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

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,
    );
  }
}

从代码可以看出,和 beginWork 一样,他也是通过 current === null ?区分 mount 与 update。

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

effectTag

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

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

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

那么,如果要通知 Renderer 将 Fiber 节点对应的 DOM 节点插入页面中,需要满足两个条件:

  1. fiber.stateNode 存在,即 Fiber 节点中保存了对应的 DOM 节点

  2. (fiber.effectTag & Placement) !== 0 ,即 Fiber 节点存在 Placement effectTag

我们知道,mount 时,fiber.stateNode === null,且在 reconcileChildren 中调用的 mountChildFibers 不会为 Fiber 节点赋值 effectTag。那么首屏渲染如何完成呢?

针对第一个问题, fiber.stateNode 会在 completeWork 中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设 mountChildFibers 也会赋值 effectTag,那么可以预见 mount 时整棵 Fiber 树所有节点都会有 Placement
effectTag。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的 DOM 操作是极低效的。

为了解决这个问题,在 mount 时只有 rootFiber 会赋值 Placement effectTag,在 commit 阶段只会执行一次插入操作。

completeWork

流程图:

类似 beginWork,completeWork 也是针对不同 fiber.tag 调用不同的处理逻辑。

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;
    }
    // ...省略
  }
  // ...省略
}

我们重点关注页面渲染所必须的 HostComponent (即原生 DOM 组件对应的 Fiber 节点),其他类型 Fiber 的处理留在具体功能实现时讲解。

处理 HostComponent

beginWork 一样,我们根据 current === null ?判断是 mount 还是 update

同时针对 HostComponent ,判断 update 时我们还需要考虑 workInProgress.stateNode != null ?(即该 Fiber 节点是否存在对应的 DOM 节点)

case
HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update 的情况
    // ...省略
  } else {
    // mount 的情况
    // ...省略
  }
  return null;
}

update 时

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

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

我们去掉一些当前不需要关注的功能(比如 ref)。可以看到最主要的逻辑是调用 updateHostComponent 方法。

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

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

workInProgress.updateQueue = (updatePayload: any);

其中 updatePayload 为数组形式,他的偶数索引的值为变化的 prop key,奇数索引的值为变化的 prop value。

mount 时

同样,我们省略了不相关的逻辑。可以看到,mount 时的主要逻辑包括三个:

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

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

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 树插入页面的呢?

原因就在于 completeWork 中的 appendAllChildren 方法。

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

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 为起点的单向链表。

                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

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

流程结尾

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

commitRoot(root);

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

如日中天

暂无简介

文章
评论
641 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文