前言
最近看了些掘金各位前端大佬的React源码文章,根据自己去源码的理解,总结了React的更新机制。也许并非是全部正确的,如果有错误或者理解偏差的地方,希望各位大佬可以在评论区提出一些自己的见解。
React的更新机制
根据我目前所了解的React的机制,我将其分为五个阶段
schedul
空间调度阶段reconcile
转化阶段beginWork
diff比对阶段completeWork
复用节点阶段commit
更新阶段
schedul阶段
schedul(空间调度)在Wiondw有一个requestIdleCallbac
函数,
requestIdleCallbac 官方文档介绍:
window.requestIdleCallback()
开发者能够在循环上执行后台和低级工作,而不会延迟关键事件,如动画和响应。会先按顺序执行函数然后timeout
有顺序调用的执行顺序,可以执行顺序,以便在前面执行函数而乱打乱执行顺序。
requestIdleCallback
接受两个参数,一个是回调函数,一个是timeout,
// 空闲调度
const workLoop = (deadline: any) => {
console.log(deadline, '空间调度的参数')
// 是否空闲
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
)
shouldYield = deadline.timeRemaining() < 1
}
// 全部转化结束,进入commit阶段
if (!nextFiberReconcileWork) {
commitRoot()
}
requestIdleCallback(workLoop);
}
\
我理解本质是,每次调用requestIdleCallback
会返回一个值,这个值是剩余时间,需要执行的任务会判断剩余时间是否足以支撑这个任务完成,如果不足以支撑,则暂停。如果足以支撑则继续执行任务。
这个其实就是React Fiber 出现的意义。之前React 是通过递归来进行更新操作,这个操作是不可打断的,如果打断了,下次将重新递归。这样的机制,就会造成页面的卡顿,对用户的体验不好
根据一些文章,React的底层其实并没有采用requestIdleCallback
这个函数,而是从底层自己实现了一套requestIdleCallback
空间调度的函数
schedul阶段
主要做了什么?
判断虚拟DOM转化为Fiber的时间是否充足,不充足就停止转换。时间充足则一直转化,直到全部转换结束
reconcile阶段
将Vdom 转化为Fiber 并通过一定的结构,将每个Fiber工作单元相连接,形成一个链表
function performNextWork(fiber: any) {
console.log(fiber,'fibelfibelfibel');
reconcile(fiber);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
在上述代码中,可以看到转化的流程 ,我将其分为3个流程
-
先找fiber的child(子元素),子元素存在,将子元素作为一个fiber工作单元return出去,重新执行schedule
-
再找fibel的sibling(兄弟元素),兄弟元素存在,将兄弟元素作为一个fiber工作单元return出去,重新执行schedule
-
最后return fibel的父节点,
在这个过程中,每转化一个Vdom 变成一个Fiber工作单元,都会重新去执行一下schedule
,判断时间是否充足,如果不充足,停止转化,先渲染页面。等待下帧在继续转化,直到全部转化完成,执行commit阶段,在下帧重新渲染页面,如果充足,继续转化,全部转化完成,执行commit,重新渲染页面。
reconcile
过程中,也会根据Fiber去创建DOM节点。最后会把所有Fiber工作单元连接成一个链表,通俗点来讲就是一个Fiber树。
beginWork
阶段,通过diff 算法 对比两棵树。
单节点diff比对
- 如果同级节点不相同,直接放弃旧的Fiber节点,创建新的Fiber节点
- 如果同级节点相同,对比key 如果key相同,则替换Fiber节点的props属性
- 按照相同的比对方式,一直校验,直到全部Fiber节点全部对比结束
多节点diff比对
- 判断同级节点是否相同,如果不相同,直接放弃oldFiber,创建新的Fiber节点
- 如果节点相同,需要判断是否Fiber节点移动了位置,如果移动了位置,则打上标记
- 如果节点相同,也没有移动位置,则只替换Fiber节点的props属性
beginWork
阶段涉及到两个问题,为什么React的元素都要有一个key,为什么不建议key使用索引(index),
在我的理解中,React 进行diff的比对阶段,如果两个元素的key相同,说明是相同的元素,可以直接替换Dom的props属性,将其更新,不需要重新去生成DOM,节省了性能,如果是index索引的话,index会随着数组的长度而改变。我们使用index作为唯一的标识,如果数组的长度加1或者减1,或者是某个元素移动了位置,相对应的index 也会发生改变,React比对的情况下,会删除掉之前的元素,重新创建一个相同的元素
这里有个问题:如果不设置key值,React diff阶段会先比对 标签,也就是babel中的第一个参数,设置key为index 就跳过比对标签这一步了吗?这里继续下,后续重新学习React,要注重关注这个问题
completeWork
阶段
这个是diff的附属阶段,会进行一次向上比对,尽可能的复用oldFiber 提高性能,同时也会将已经对比结束的Fiber树加入更新队列,在commit阶段完成渲染
commit
更新DOM
// commitRoot
function commitRoot() {
commitWork(wipRoot?.child);
wipRoot = null
}
// commitWork
function commitWork(fiber: any) {
if (!fiber) {
return
}
let domParentFiber = fiber.return
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.return
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
commit
阶段会根据Fiber树的结构child,sibling, return进行渲染,并且插入到DOM中,因为在reconcile
阶段DOM已经创建好了,并且通过diff阶段也知道了是删除,新增,移动的哪一项操作,所以commit 阶段会很快
渲染视图,React 更新结束
博主也是一个初级的React的小白,这个只是博主自己对React 更新机制的理解,各位仅供参考,如果错误,或者更好的见解,可以在评论区指导下博主。