React渲染流程

1,494 阅读4分钟

上文我们讲过React的核心架构由调度器,协调器,渲染器组成。这期我们讲下渲染流程。

render和commit

react的渲染流程分为两个阶段。

  • render 阶段:调合虚拟 DOM,计算出最终要渲染出来的虚拟 DOM
  • commit 阶段:根据上一步计算出来的虚拟 DOM,渲染具体的 UI

每个阶段对应不同的组件:

image.png

  • 调度器(Scheduer):调度任务,为任务排序优先级,让优先级高的任务先进入到 Reconciler
  • 协调器(Reconciler):生成 Fiber 对象,收集副作用,找出哪些节点发生了变化,打上不同的 flags,diff 算法也是在这个组件中执行的。类组件的render和函数组件也是在这里被调用的
  • 渲染器(Renderer):根据协调器计算出来的虚拟 DOM 同步的渲染节点到视图上。

举个例子:

export default function App() {
  const [count, updateCount] = useState(0);
  return (
    <ul>
      <button onClick={() => updateCount(count + 1)}>add</button>
      <span>count is: {count}</span>
    </ul>
  );
}

当点击了add按钮发生了什么呢? 首先是由 Scheduler 进行任务的协调,render 阶段(虚线框内)的工作流程是可以随时被以下原因中断:

  • 有其他更高优先级的任务需要执行
  • 当前的 time slice 没有剩余的时间
  • 发生了其他异常情况,比如代码报错

在render阶段的工作都是在内存中进行的,计算出更新后的fiber tree,但是并没有更新UI,在这个阶段视图不会有任何更改。

当 Scheduler 调度完成后,将任务交给 Reconciler,Reconciler 就需要计算出新的 UI,最后就由 Renderer 同步进行渲染更新操作。(渲染是同步执行的,不可以被打断!!!)

大致流程为:

graph TD
点击按钮 --> 触发updateCount --> Scheduler接收到任务 --> Scheduler开始调度 --> 
Reconciler计算更新 --> span被打上update的flag --> Renderer根据计算出来的fiber树更新真实DOM --> 用户看到span改变了

具体的细节后面会再给大家介绍,特别是Reconciler是如何做Diff的。

Reconciler是如何协调的?

Reconciler会根据调度结果执行同步或者并发更新

  • performSyncWorkOnRoot(同步更新流程)
  • performConcurrentWorkOnRoot(并发更新流程)

Reconciler是通过递归的方式来创建新增的Fiber tree。

看下源码的差别

// performSyncWorkOnRoot 会执行该方法
function workLoopSync(){
  while(workInProgress !== null){
    performUnitOfWork(workInProgress)
  }
}
// performConcurrentWorkOnRoot 会执行该方法
function workLoopConcurrent(){
  while(workInProgress !== null && !shouldYield()){
    performUnitOfWork(workInProgress)
  }
}

唯一的区别就是否调用shouldYield方法。该方法表明了是否可以中断。

源码中performUnitOfWork 方法会创建下一个 FiberNode,并且还会将已创建的 FiberNode 连接起来(child、return、sibling),从而形成一个链表结构的 Fiber tree。

workInProgress 代表的是当前的 FiberNode。当workInProgressnull说明整颗Fiber tree构建完成。

**但是工作还没有完成结束。**上面的流程称为beginWork, 还需一个completeWork流程去收集副作用。

在React中,completeWork阶段是为了收集副作用( side effects)。副作用是指那些不会直接改变组件状态或数据的操作,例如读取文件、发送网络请求、修改DOM等。这些操作通常需要在组件渲染时执行,并且可能会对组件的状态或数据产生影响。

在React中,副作用通常由useEffect钩子函数来处理。useEffect函数接受两个参数:依赖项数组和回调函数。依赖项数组用于指定当依赖项发生变化时,副作用函数会被重新调用。回调函数则是实际执行副作用操作的函数。

completeWork阶段,React会将副作用函数收集起来,并在下一次渲染时执行它们。这样做的好处是可以避免在每次渲染时都执行副作用操作,从而提高性能。同时,副作用操作也可以确保在组件卸载时能够正确地清理资源。

总结起来就是performUnitOfWork深度优先在创建Fiber tree时也会分成两个阶段。

  • beginWork递阶段(向下生成并连接Fiber Node)
  • complateWork归阶段(向上收集Fiber的副作用)

如下结构

<ul>
  <li>1</li>
  <li>2</li>
</ul>
graph TD
beginWork --> 生成ul --> 生成li1 --> 
complateWork收集li1副作用 --> 生成li2 --> complateWork收集li2副作用
--> 完成

image.png

这里看着吃力的话需要去补习一下深度和广度优先搜索

渲染器的工作流程

渲染器并不只是简简单单的更新节点,他也要做很多的事情。相比之前的render阶段渲染器是不可以被打断的。

image.png

整个渲染器渲染过程中可以分为三个子阶段:

  • BeforeMutation 阶段 在这个阶段,React会对组件树进行一些优化和准备工作。
  • Mutation 阶段 React会对组件的子树进行更新,包括创建、销毁和移动子节点等操作。
  • Layout 阶段 React会对组件进行实际的布局操作。useLayoutEffect就是在这里被调用了,此时Fiber tree已经被替换了。但是仍然没有更新真实的DOM,所以useLayoutEffect获取不到最新的DOM