文章目录
4. render
在 scheduler 阶段根据每一个调度任务的优先级(过期时间)筛选出一个调度任务,进入 render 阶段进行构建或者更新 workInProgress 树。render 阶段是对 DOM 对应的 current Root 深度优先遍历后,分别进行了 创建更新 workInProgress、遍历树剪枝操作、执行创建对应的组件、收集Effect、更新孩子过期时间、completeWork 、中断错误挂起处理 等。最终如果没有被其他任务中断的情况下,会进入 commit 阶段同步更新 DOM。
(基于 v16.13.1 版本)
render 整体流程
render 整体流程是基于经典的 DFS 算法非递归实现的,在算法中分为访问和回溯两个大的过程,在访问到某个节点的时候,会根据节点的类型执行相应的动作产出 child,在回溯过程中,会收集被回溯节点的孩子节点相关的DOM、effect、expirationTime 等。
同时 render 过程也是基于 double buffer 模式的,所以存在 current workInProgress 两个fiber互相对应, 但是 render 阶段是根据 current 上收集到的副作用、变化更新创建出下一个要 commit 的 fiber 树 — workInProgress。粗略的介绍该创建更新流程。
DFS 框架遍历
整体的 render 是一个 DFS 非递归深度遍历的过程 ,大概整个流程如下代码所示:
const DFS = (root) => {
while (root) {
nextRoot = root.child;
if (nextRoot) {
// 处理孩子节点
root = nextRoot;
} else {
while (true) {
if (root.sibling) {
// 处理兄弟节点
root = root.sibling;
break;
} else if (root.return) {
// 回溯找到待需要处理兄弟节点的父节点
root = root.return;
} else {
// 整棵树遍历完成
root = null;
return;
}
}
}
}
};
先深入到一条通路直到到达叶节点为止,然后回溯找到该通路上的兄弟节点,最终遍历完成整棵树。整个过程分为两个部分 访问 和 回溯。所以可以得到如下的结论:
- 树中每个节点都经历过访问和回溯的过程。
- 被回溯的节点其所有的孩子节点应该都被访问和回溯过。
- 当回溯到节点为空时,整棵树回溯完成。
- 当下一个访问节点为空时,从根节点到该节点的路径上节点都被访问过。
关键处理方法
react 的 render 过程正是使用 DFS 非递归遍历的特性:访问、回溯,分别进行处理使得一趟遍历就会收集到当前调度的所有需要被更新的节点 effect。从而为 commit 阶段提供 effect 更新列表,从而避免了 commit 阶段再一次遍历所有节点进行 effect 收集工作。将 react 源码进行抽象后,得到如下代码:
const beginWork = (current, workInProgress) => {
// 根据不同的 type 处理不同逻辑
return workInProgress.child;
};
const completeUnitOfWork = (workInProgress) => {
while(true) {
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
completeWork(workInProgress);
resetChildExpirationTime(workInProgress);
setEffectToReturn(workInProgress);
if (siblingFiber !== null) {
return siblingFiber;
} else if (returnFiber !== null) {
workInProgress = returnFiber;
continue;
} else {
return null;
}
}
};
const wookLoop = (workInProgress) => {
const current = workInProgress.alternate;
let next;
next = beginWork(current, workInProgress);
if(!next) {
next = completeUnitOfWork(workInProgress);
}
return next;
};
const renderRoot = (root) => {
let nextUnitOfWork = root.workInProgress;
do {
try {
nextUnitOfWork = wookLoop(nextUnitOfWork);
} catch (error) {
// error、suspend
}
}while(true);
}
renderRoot(root);
这部分代码是 上边 DFS 代码的扩展,其中 beginWork 对应了 访问 的过程,completeUnitOfWork 代表了 回溯 的过程。其中:
-
beginWork 是处理节点生成 child 的过程。比如 函数组件将运行函数得到孩子节点,或者 执行 类组件的 render 方法生成孩子节点;得到的孩子节点将会作为 beginWork 接下来处理的对象。如果 child 是 null 代表该路径已经遍历完成了需要准备回溯,要执行 completeUnitOfWork 了。
-
completeUnitOfWork 是回溯收集元素 DOM 变更,修改 child 过期时间 以及 收集 effect 的过程。并且在这个过程中,已经确保被处理的节点的所有孩子节点都被 beginWork 和 completeUnitOfWork 处理过,也就是都被访问、回溯过。
-
completeWork 是将其所有第一次层孩子节点的 DOM 实例 appendChild 到该节点 DOM 实例上的过程,之所以是第一层,是因为其孩子节点已经完成了他们孩子节点的 appendChild 工作,这个过程接下来会做具体的介绍。
-
resetChildExpirationTime 将所有的孩子节点以及孩子节点上的childExpirationTime比较找出过期时间最短的值,并且重新设置该节点。这个过程也会在下面具体分析。
-
setEffectToReturn 同上两个方法,其过程也是收集该节点所有孩子节点的 effect 的信息,比如回调函数的执行、生命周期方法执行、删除某节点等。为 commit 阶段作为数据源。
-
当然如果在整个过程中,出现了异常错误,或者时间资源被回收,或者被中断,都会进入到 suspend(挂起) 或者 error(错误) 阶段。还阶段在 render 过程中不做介绍。