前面介绍了 React 的整体架构和 React 当中重要的数据结构 Fiber,以及 Fiber 的源头 element 对象和 FiberRoot 根对象,这一章开始讲解 React 核心工作过程的源码。
在讲解源码的过程中,最常提到的两个全局变量:
current,虚拟 DOM 树中,diff 算法遍历到的旧 Fiber 节点(不同于fiberRoot.current);workInProgress,将要替换current的新 Fiber 节点。
假设 React Reconcile 现在正处在一个组件的更新阶段,React 已经通过 useState 的 dispatch,再调用 scheduleUpdateOnFiber 创建了一次更新,更新的过程是这样的:
图中 Fiber1 就是这次更新的组件在 current 树中对应的虚拟DOM,scheduleUpdateOnFiber 方法作用于它。第五步替换发生在 commit 阶段,不包含在 workLoop 中。
第一轮,完成1和2,生成 new Fiber1;第二轮,完成3和4,生成 new Fiber2,如果还有子组件和兄弟组件,则还会有下一轮。React 就是这样将一次对比与生成的过程看成一次 work,然后不断地循环,这就是 workLoop。
当前这轮 work 当中生成的 Fiber 称为 workInProgressFiber,源码中常常简称 workInProgress,显然,每一轮它都会指向不同的新 Fiber。
Concurrent 模式引入后 workLoop 有两种,一种是旧的 workLoopSync,一种是新的 workLoopConcurrent,唯一区别就是 workLoopConcurrent 可以被 Schedule 暂停。
function workLoopSync() {
while (workInProgress !== null) {
// workInProgress 全局属性每次循环都会被修改
performUnitOfWork(workInProgress);
}
}
...
import { shouldYield } from './Scheduler';
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
performUnitOfWork首先会执行beginWork生成当前workInProgress的所有第一级子节点,并返回第一个子节点,此时第一个子节点会成为下一个workInProgress;如果没有子节点则返回 null;beginWork返回值不为 null,则一直循环执行beginWork,直至返回 null;beginWork返回值为 null,则执行completeUnitOfWork,对当前workInProgress的所有第一级子节点进行FLAG类属性向上冒泡操作,并把其return属性指向workInProgress。
const current = workInProgress.alternate;
let next;
// 主要作用是为`workInProgress`生成子节点
next = beginWork(current, workInProgress, subtreeRenderLanes);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (next === null) {
// 主要作用是定义 workInProgress 关于子节点的 flag 属性(属性冒泡),
// 并把子节点 return 属性指向 workInProgress
completeUnitOfWork(workInProgress);
} else {
workInProgress = next;
}
- 进入
completeUnitOfWork的workInProgress如果没有子节点则返回下一个兄弟节点,此时下一个兄弟节点会成为新的workInProgress,重新进入beginWork阶段; - 进入
completeUnitOfWork的workInProgress如果没有下一个兄弟节点,则继续对其父节点进行completeUnitOfWork过程。
let completedWork = workInProgress
do {
// completedWork(workInProgress)在 current 树中对应的节点
const current = completedWork.alternate;
// completedWork(workInProgress)父节点
const returnFiber = completedWork.return;
let next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
// 如果 next 存在,则将 workInProgress 指向 next
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// next 为 null,siblingFiber 不为 null,
// 则将 workInProgress 指向 siblingFiber
workInProgress = siblingFiber;
return;
}
// 该 workInProgress 没有兄弟节点,则将 completeWork 指向其父节点
// 并继续这个循环
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
总结
performUnitOfWork会循环执行beginWork和completeWork;beginWork只负责生成子节点,并返回第一个子节点;completeWork只负责子节点flag类属性的冒泡;Fiber树的遍历需要依靠completeUnitOfWork调度beginWork和completeWork两个函数;- workLoop 借助
workInProgress这个变量,根据更新的组件在内存中勾勒出一棵局部的新 Fiber 树。
图解
假设有以下 React Fiber 树,其中每个圆角方框都代表一个 Fiber 节点;接下来将截取其生成过程的一部分来说明 beginWork 和 completeWork 运作模式:
生成过程:椭圆代表当前 workInProgress 所指向的 Fiber 节点;performUnitOfWork 带着 App 进入了 beginWork 阶段,beginWork 阶段创建了 A 和 C,并将 App 的 child 属性指向 A 和 C;A 的 sibling 属性指向 C;
当 performUnitOfWork 第一次执行,beginWork 返回 A 之后,workInProgress 指向 A,因此 workInProgress !== null,performUnitOfWork 带着 A 进入 beginWork 阶段,beginWork 阶段创建了 B,并把 A 的 child 属性指向 B;
当 performUnitOfWork 第二次执行,beginWork 返回 B 之后,workInProgress 指向 B,因此 workInProgress !== null,performUnitOfWork 带着 B 进入 beginWork 阶段,beginWork 阶段根据 tag 属性做一些特殊处理,一直到 beginWork 内部 reconcileChildFibers 函数执行时,判断无子节点并返回 null;
此时生成的 Fiber 树结构为:
因为 beginWork 返回 null,此时 workInProgress 仍指向 B,performUnitOfWork 会调用 completeUnitOfWork 函数,completeWork 函数带着 B 节点进入 completeWork 阶段,completeWork 函数多数情况只会冒泡子节点属性并返回 null,因为 B 没有子节点,所以直接返回 null;
completeUnitOfWork 内部检查 B 节点是否有兄弟节点,发现 B 节点没有兄弟节点,因此completeWork 函数带着 A 节点进入 completeWork 阶段,将其子节点部分属性冒泡到 A 节点,并将其所有子节点 return 属性指向 A;
对节点 A completeWork 过程结束后,completeUnitOfWork 查询 A 是否有兄弟节点,发现有兄弟节点 C,因此返回 C;
completeUnitOfWork 返回节点 C,因此 workInProgress 指向 C,C 会跟之前 A 节点一样,经历同样的 beginWork 和 completeWork 过程,并且因为 C 没有兄弟节点,因此下一个进行 completeWork 的是它的父节点 App,App 进入 completeWork 之前生成的 Fiber 树结构为:
App 进入 completeWork 阶段,冒泡子节点 A、C 部分属性,并将 A、C 节点 return 属性指向 App;