狗教我React——原理篇之整体渲染流程React的渲染流程分为两个阶段: 1. render 阶段:Reconcile
前置知识:深度优先搜索(DFS)、Fiber节点
- Scheduler(调度器):根据任务的优先级安排任务执行顺序。
- Reconciler(协调器):根据新旧虚拟DOM树的差异确定需要更新的部分。
- Renderer(渲染器):将更新的虚拟DOM转换为实际的UI输出。
这三个组件共同工作提供了React的高效和灵活的渲染机制,那么他们具体是怎么渲染的呢?
React的渲染的两个阶段
React的渲染流程分为两个阶段:
- render 阶段:Reconciler的工作阶段,这个阶段会调用组件的render方法
- commit 阶段:Renderer的工作阶段,可以类比git commit提交,这个阶段会渲染具体的 UI。
先以这个两个阶段的整体工作流程举例:
export default function App() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<h3>{count}</h3>
<button onClick={handleIncrement}>点击加一</button>
</div>
);
}
如上图所示,当用户点击按钮更新count,Scheduler 先进行任务的协调,当 Scheduler 调度完成后,将任务交给 Reconciler,Reconciler 就需要计算出新的 UI,最后就由 Renderer 同步进行渲染更新操作。
Scheduler和Reconciler的工作流程是可以随时被以下原因中断:
- 有其他更高优先级的任务需要执行
- 当前的 time slice 没有剩余的时间
- 发生了其他错误
Scheduler和Reconciler的的工作是在内存里进行的,不会更新用户界面,因此即使工作流程反复被中断,用户也不会看到更新不完全的UI。
由于Scheduler和Reconciler都是平台无关的,所以
React
为他们单独发了一个包react-Reconciler
调度器Scheduler
上篇文章提到,Fiber和Scheduler都是React16引入的。Scheduler是用来根据任务的优先级安排任务执行顺序的。
其实部分浏览器的原生API已经实现了,即requestIdleCallback
。
但是由于 浏览器兼容性 和 触发频率受很多因素影响而不稳定 等问题,React
放弃使用浏览器原生的API,React
实现了功能更完备的requestIdleCallback
Polyfill,即Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。
另外,Scheduler是独立于React
的库,可以用来实现任务调度,而不只是在 React 中使用。
注:Polyfill 是指用于在旧版本浏览器中实现新标准 API 的代码填充(或称垫片)。它通常用于解决旧版本浏览器不支持新特性或 API 的问题。
协调器Reconciler与Render阶段
Reconciler实现可中断的循环
Reconciler根据新旧虚拟DOM树的差异确定需要更新的部分。
上一篇文章说到,在 React15 中Reconciler是递归处理虚拟 DOM 的。而React16中,更新工作从递归变成了可以中断的循环过程。
- 每次循环都会调用
shouldYield
判断当前是否有剩余时间。如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。 - Reconciler 与 Renderer 不再是交替工作。当 Scheduler 将任务交给 Reconciler 后, Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记,整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。
Render阶段
类组件或者函数组件本身就是在render阶段被调用的。在源码中,render阶段开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用,这取决于本次更新是同步更新还是异步更新。
-
performSyncWorkOnRoot:同步模式
-
performConcurrentWorkOnRoot:并发模式
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
对于以上代码的注释:
workInProgress : 当前已创建的workInProgress fiber
,即在内存中构建的Fiber树
。(具体fiber双缓存相关后面文章细讲)
shouldYield: 如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。(可以看到上面两种方法的区别是是否调用shouldYield)
performUnitOfWork: 创建下一个Fiber节点
并赋值给workInProgress
,并将workInProgress
与已创建的Fiber节点
连接起来构成Fiber树
。
可以看到上面两种方法主要都是在执行performUnitOfWork
,下面我们详细看一下performUnitOfWork
。
performUnitOfWork方法
performUnitOfWork方法的工作流程可以分为两个阶段:“ 递 ” 和 “ 归 ”。
“递阶段 —— beginWork”
作用:传入
当前Fiber节点
,创建子Fiber节点
。
首先从rootFiber
开始向下深度优先遍历。为遍历到的每个Fiber节点
调用beginWork方法(此方法后续详细介绍),该方法会根据传入的Fiber节点
创建子Fiber节点
,并将这两个Fiber节点
连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
“归阶段 —— complateWork”
作用:收集一些副作用。
在“归”阶段调用completeWork处理Fiber节点
,主要是收集一些副作用(此方法后续详细介绍)。
当某个Fiber节点
执行完completeWork
,如果其存在同级Fiber节点
(即fiber.sibling !== null
),会进入其同级Fiber
的“递”阶段。
如果不存在同级Fiber
,会进入父级 Fiber
的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber
。至此,render阶段
的工作就结束了。
图示 “ 递 ” 和 “ 归 ”
先看一个简单的:
稍复杂的fiber节点。注意beginWork和complateWork的顺序:
注:为什么指向父级 fiber( parent FiberNode )的字段叫做 return 而不是 parent? 因为作为一个工作单元,
return
指节点执行完completeWork
后会返回的下一个节点。子Fiber节点
及其sibling节点完成工作后会返回其父级节点(parent FiberNode),所以用return
指代父级节点。
渲染器Renderer与commit阶段
render阶段完成后,开启commit阶段
工作流程,Renderer在此阶段工作。
与 render 阶段可以被打断不同的是,commit 阶段是不可以被打断的,一旦开始就会同步执行直到完成渲染工作。
渲染器Renderer的工作主要就是将各种副作用(flags 表示)commit 到宿主环境的 UI 中。整个阶段可以分为三个阶段,分别是 BeforeMutation 阶段、Mutation 阶段和 Layout 阶段。
- before mutation 阶段(执行
DOM
操作前):一些准备工作,如处理DOM节点渲染/删除后的autoFocus
、blur
逻辑、触发getSnapshotBeforeUpdate
生命周期方法、调度useEffect
。 - mutation 阶段(执行
DOM
操作):React根据调和阶段的计算结果执行DOM的增删改操作。 - layout 阶段(执行
DOM
操作后):执行一些可能需要最终的 DOM 结构信息才能完成的工作,比如测量 DOM 元素的尺寸和位置。
注意:在
before mutation阶段
之前和layout阶段
之后还有一些额外工作,涉及到比如useEffect
的触发、优先级相关
的重置、ref
的绑定/解绑。
此篇为学习记录,如有错误欢迎纠正~
转载自:https://juejin.cn/post/7400584032621772800