上文我们讲过React的核心架构由调度器,协调器,渲染器组成。这期我们讲下渲染流程。
render和commit
react的渲染流程分为两个阶段。
- render 阶段:调合虚拟 DOM,计算出最终要渲染出来的虚拟 DOM
- commit 阶段:根据上一步计算出来的虚拟 DOM,渲染具体的 UI
每个阶段对应不同的组件:
- 调度器(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。当workInProgress
为null
说明整颗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副作用
--> 完成
这里看着吃力的话需要去补习一下深度和广度优先搜索
渲染器的工作流程
渲染器并不只是简简单单的更新节点,他也要做很多的事情。相比之前的render阶段渲染器是不可以被打断的。
整个渲染器渲染过程中可以分为三个子阶段:
- BeforeMutation 阶段 在这个阶段,React会对组件树进行一些优化和准备工作。
- Mutation 阶段 React会对组件的子树进行更新,包括创建、销毁和移动子节点等操作。
- Layout 阶段 React会对组件进行实际的布局操作。
useLayoutEffect
就是在这里被调用了,此时Fiber tree已经被替换了。但是仍然没有更新真实的DOM,所以useLayoutEffect
获取不到最新的DOM