流程
入口
ReactDOM.render(<App />, rootNode) 同步
lane === SyncLane 这个条件是成立的,因此会直接进入 performSyncWorkOnRoot 的逻辑,开启同步的 render 流程;而在异步渲染模式下,则将进入 else 的逻辑。
ReactDOM.createRoot(rootNode).render(<App />) 异步
else 逻辑 还需要分
ensureRootIsScheduled 这个方法
该方法很关键,它将决定如何开启当前更新所对应的 render 阶段
ensureRootIsScheduled 中 核心逻辑
if (newCallbackPriority === SyncLanePriority) {
// 同步更新的 render 入口
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 将当前任务的 lane 优先级转换为 scheduler 可理解的优先级
var schedulerPriorityLevel =
lanePriorityToSchedulerPriority(newCallbackPriority);
// 异步更新的 render 入口
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
React会以当前更新任务的优先级类型为依据,决定接下来是调度performSyncWorkOnRoot还是performConcurrentWorkOnRoot。
初始化
-
jsx的转换 -
FiberRoot构建(OnlyOne)- ReactDOM.render()会产生多个
rootFiber - fiberRoot 和 rootFiber 建立起关联
- ReactDOM.render()会产生多个
首次渲染时采用
ReactSyncRoot进行同步渲染,不会进入异步调度过程,因为组件需要尽快的完成渲染。最终渲染完成后生成一颗完整的 fiber 树。
- 完整的 FiberTree
Scheduler(调度器)
调度任务的优先级,高优任务优先进入 更新
时间切片原理
接下来我们开启 Concurrent Mode(开启后会启用时间切片)
// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App />);
时间切片的本质是模拟实现
requestIdleCallback
requestIdleCallback例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
const taskList = [
{
id: 1,
msg: "first task",
},
{
id: 2,
msg: "second task",
},
{
id: 3,
msg: "third task",
},
{
id: 4,
msg: "four task",
},
];
function wait(time) {
const now = Date.now();
while (Date.now() - now < time) {
// console.log('--->');
}
}
function oldExecuteTask(list = taskList) {
list.forEach((item) => {
console.log("execute task", item.msg);
wait(1000);
});
}
function newExecuteTask(list = taskList) {
requestIdleCallback(
() => {
const firstList = list[0];
console.log(`execute task ${firstList.msg}`);
wait(1000);
list.length > 1 && newExecuteTask(list.slice(1));
},
{ timeout: 2000 }
);
}
</script>
</head>
<body>
<button onclick="oldExecuteTask()">execute old task</button>
<button onclick="newExecuteTask()">execute new task</button>
<button onclick="(() => {console.log('other task')})()">other task</button>
</body>
</html>
一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback
优先级调度
Scheduler 与 React 是两套优先级机制。在 React 中,存在多种使用不同优先级的情况,比如:
以下例子皆为 Concurrent Mode 开启情况
-
过期任务或者同步任务使用同步优先级
-
用户交互产生的更新(比如点击事件)使用高优先级
-
网络请求产生的更新使用一般优先级
-
Suspense 使用低优先级
React 需要设计一套满足如下需要的优先级机制:
-
可以表示优先级的不同
-
可能同时存在几个同优先级的更新,所以还得能表示批的概念
-
方便进行优先级相关计算
为了满足如上需求,React 设计了 lane 模型。接下来我们来看 lane 模型如何满足以上 3 个条件。
Fiber 更新过程(Fiber Reconciler(协调器))
render 阶段
new Fiber Tree生成
Fiber 节点是如何被创建并构建 Fiber 树的。
Fiber-->Fiber Tree
递阶段
首先从
rootFiber开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用beginWork方法。
该方法会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段
在
React中最多会同时存在两棵Fiber树
CurrentTree 和 WorkInProgressTree
在第一次渲染之后(mount),
react最终得到一个fiber tree
当
react开始处理更新时,它会构建一个workInProgress tree,它反映了要刷新到屏幕的未来状态。(new Fiber Tree)
beginWork 的工作可以分为两部分
update 时:如果 current 存在,在满足一定条件时可以复用 current 节点,这样就能克隆 current.child 作为 workInProgress.child,而不需要新建 workInProgress.child。
mount 时:除 fiberRootNode 以外,current === null。会根据 fiber.tag 不同,创建不同类型的子 Fiber 节点
“归”阶段
在“归”阶段会调用 completeWork (opens new window)处理 Fiber 节点。
当某个 Fiber 节点执行完 completeWork,如果其存在兄弟 Fiber 节点(即 fiber.sibling !== null),会进入其兄弟 Fiber 的“递”阶段。
如果不存在兄弟 Fiber,会进入父级 Fiber 的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到 rootFiber。至此,render 阶段的工作就结束了。
Effect
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
在 commit 阶段只需要遍历 effectList 就能执行所有 effect 了
借用 React 团队成员 Dan Abramov 的话:effectList 相较于 Fiber 树,就像圣诞树上挂的那一串彩灯。
可打断,React 在 workingProgressTree 上复用 current 上的 Fiber 数据结构来一步地(通过 requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
他们唯一的区别是是否调用
shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
双缓存
这种在内存中构建并直接替换的技术叫做双缓存
React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着 DOM 树的创建与更新。
复用
workInProgress fiber的创建可以复用current Fiber树对应的节点数据。
workInProgress Fiber树在 render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树
Renderer(渲染器)
commit阶段
commit 阶段的主要工作(即 Renderer 的工作流程)分为三部分
-
before mutation 阶段(执行 DOM 操作前)
-
mutation 阶段(执行 DOM 操作)
-
layout 阶段(执行 DOM 操作后)
commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。
类似
git commit提交代码
在
commit阶段会触发一些生命周期钩子(如 componentDidXXX)和 hook(如useLayoutEffect、useEffect)。
Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。
useLayouEffect与useEffect的区别
在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程
更新粒度
树级更新
采用这种更新方式最有名的框架就是
React
React每次更新都会从rootFiber(根 Fiber 节点)向下深度优先遍历, 通过alternate复用 再次重新生成一颗Fiber Tree
「树级更新」的框架会再生成一棵完整「虚拟 DOM 树」,生成过程中与之前的「虚拟 DOM 树」对应节点进行比较: 依赖「虚拟 DOM」
不关心触发更新的节点(因为会通过「虚拟 DOM」的全树对比找到他)
组件级更新
采用这种更新方式最有名的框架就是
Vue
会找到触发更新节点所在组件,生成该组件的「虚拟 DOM 树」(而不是全树的「虚拟 DOM 树」),生成过程中与该组件之前的「虚拟 DOM 树」对应节点进行比较
依赖「虚拟 DOM」关心触发更新的节点(「虚拟 DOM」的对比会作用于该节点所在组件)
节点级更新
采用这种更新方式最有名的框架就是 Svelte。
如果是「节点级更新」框架,在编译时会根据「状态变化对应的 DOM 变化」直接生成对应方法,当状态改变后直接调用对应方法。
不依赖「虚拟 DOM」,依赖预编译(建立状态与改变 DOM 的方法之间的联系)
关心触发更新的节点(节点状态与更新方法一一对应)
VUE3
Vue 作为「组件级更新」代表,更新粒度介于「树级」与「节点级」之间。那到底是中间偏左呢,还是中间偏右呢?
我要反复横跳,两边我都要
当使用 JSX 时,Vue3 拥有了 React 运行时的灵活性,此时的 Vue3 可以看作是「加强版 React + Mobx」
注意
concurrent,面向未来的开发模式。我们之前讲的任务中断/任务优先级都是针对 concurrent 模式
更新链路疏通同归
mount
首次同步生成的 Fiber Tree
update