Skip to content

Render流程概述

  • Trigger
    触发渲染
  • Render
    根据组件返回的 JSX 在内存中依次创建 Fiber节点 并连接在一起构建 Fiber树,被称为 workInProgress Fiber树
  • Commit
    workInProgress Fiber 树在 Render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树。

Fiber节点是如何被创建并构建Fiber树的

Render

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

js
// 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树。

workLoopConcurrent源码

js
// performSyncWorkOnRoot
function performSyncWorkOnRoot(root) { 
  // ...
  let exitStatus = renderRootSync(root, lanes);
  // ...
}
js
// performConcurrentWorkOnRoot
function performConcurrentWorkOnRoot(root, didTimeout) {
  // ...
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  // ...
}
js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { 
  // ...
  workLoopConcurrent();
  // ...
}

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  // ...
  workLoopSync();
  // ...
}

Fiber Reconciler 通过遍历方式实现可中断的递归,performUnitOfWork 可以分为两部分:

  • 递阶段

    rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber节点 调用 beginWork 方法

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

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

  • 归阶段

    在“归”阶段会调用 completeWork 处理 Fiber节点

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

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

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

作为一种性能优化手段,针对只有单一文本子节点的 FiberReact 会特殊处理。

beginWork

beginWork 代码看这里

beginWork 的工作是传入 当前Fiber节点,创建 子Fiber节点

js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...函数体
}

其中传参:

  • current 当前组件对应的 Fiber节点 在上一次更新时的 Fiber节点,即 workInProgress.alternate
  • workInProgress 当前组件对应的 Fiber节点
  • renderLane 优先级相关 Scheduler

组件 mount 时,由于是首次渲染,不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点。此时current === null,因此通过 current === null ? 来区分组件是处于 mount 还是 update

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

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

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

js
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)

  1. oldProps === newProps && workInProgress.type === current.type,即 propsfiber.type 不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前 Fiber节点 优先级不够
js
  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;
  }

mount时

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

根据 fiber.tag 不同,进入不同类型 Fiber 的创建逻辑。

js
// 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

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

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

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

completeWork

completeWork 代码看这里

流程概述

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

js
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

HostComponent 即原生 DOM组件 对应的 Fiber节点

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

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

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

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

update

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

例如:

  • onClickonChange 等回调函数的注册
  • 处理 style prop
  • 处理 DANGEROUSLY_SET_INNER_HTML prop
  • 处理 children prop
js

updateHostComponent 方法定义

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

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

typescript
workInProgress.updateQueue = (updatePayload: any);

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

mount

mount时的主要逻辑包括三个

  • Fiber节点 生成对应的 DOM节点
  • 子孙DOM节点 插入刚生成的 DOM节点
  • update 逻辑中的 updateHostComponent 类似的处理 props 的过程
js
// 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,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

mount 时只会在 rootFiber 存在 Placement effectTag

commit阶段 是如何通过一次插入DOM操作(对应一个 Placement effectTag )将 整棵DOM树 插入页面的呢?

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

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

effectList

至此 render 阶段的绝大部分工作就完成了。

completeWork 的上层函数 completeUnitOfWork 中,每个执行完 completeWork 且存在 effectTagFiber节点 会被保存在一条被称为effectList的单向链表中。

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

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

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

commit阶段 只需要遍历 effectList 就能执行所有 effect

代码逻辑见此处

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

js
commitRoot(root);

commitRoot(root)


参考图片

beginWork 流程图

beginWork

completeWork 流程图

completeWork