前言
这篇文章整整拖延了一年时间,终于把它更新上来了,看了无数个关于React源码阅读的视频,看了很多个技术的帖子,目前总结出更加容易理解的熟悉掌握住整个 React
流程的讲解文章,目的是让你在脑海中至少掌握清楚 React
的流程的。如果存在不合理的地方欢迎在评论区留言。
本文主要介绍的是,React
创建更新以后 进入调度流程,整体完整的介绍,React 的调度与工作流程是其核心逻辑的重要组成部分。自从 React 16(Fiber 架构)后,这部分逻辑变得尤为关键,因为 React 引入了时间切片和优先级概念,允许渲染工作被打断和重新调度。
判断 Expiration Time
是否同步,同步的话执行 performSyncWork
,否则执行scheduleCallbackWithExpirationTime
。 这两步逻辑,对于后续的更新差异是很大的。 同步更新的话,需要马上更新到 DOM Tree
上面,异步的话说明它的优先级不是特别高,任务是可以被打断的,那么它就会进入调度的流程。具体的 Expiration Time
计算流程可以 查看往期的文章。
以下是 React Scheduler 和 Work 的大致流程,以及部分相关代码片段(这只是一个简化和概览,实际源代码结构和内容更为复杂)
整体流程图
整体的流程
setState & useState
- 触发更新 :通常情况下,调用
setState
或useState
的 setter 会触发更新。这里就不进行详细的描述了,这个点经常在我们的开发中出现
js
function useState(initialValue) {
// ... 省略部分代码
const dispatch = (action) => {
// 更新 state 和 queue
// ...
scheduleWork(fiber, expirationTime); // 调度工作
};
// ...
}
scheduleWork
- 开始调度 :
scheduleWork
是调度更新的入口。
js
function scheduleWork(fiber, expirationTime) {
// 更新 root 上的 expirationTime
const root = scheduleWorkToRoot(fiber, expirationTime); // 详细可以看下面的代码内容
if (!isWorking && !isCommitting) {
// 如果当前没有正在执行的工作,请求一个新的工作调度
requestWork(root, root.expirationTime);
}
}
scheduleWorkToRoot
调度工作时如何找到与给定 Fiber 对应的 Root Fiber,在调度更新时,React 会从当前 Fiber 向上遍历其父级,直到找到 root。
js
function scheduleWorkToRoot(fiber, expirationTime) {
// 首先,我们将更新标记到当前 fiber 及其父级上
let node = fiber;
while (node !== null) {
// 更新该 fiber 的 expiration time
if (node.expirationTime < expirationTime) {
node.expirationTime = expirationTime;
}
// 如果 node 是一个 Root Fiber,那么我们找到了它并返回
if (node.tag === HostRoot) {
return node;
}
node = node.return;
}
return null;
}
上面是简化版,具体目的就是找 root。
requestWork
- 在 React 的更新机制中,
requestWork
函数负责根据给定的优先级请求新的工作。这一过程涉及到 React 的任务调度,尤其在 React 16(Fiber 架构)之后,这一调度机制使 React 能够更有效地利用浏览器的资源,确保高优先级的工作得到及时处理,同时在浏览器空闲时处理低优先级的工作。
以下是一个简化和概括的 requestWork
函数及相关代码:
js
// 根据 root 和 expirationTime 来请求工作
function requestWork(root, expirationTime) {
// 添加到待处理的 roots 列表中
addRootToSchedule(root, expirationTime);
if (isRendering) {
// 如果当前正在渲染,我们只是添加 root 到列表并返回
return;
}
if (isBatchingUpdates) {
// 如果正在批处理更新,等待直到批处理结束
if (isUnbatchingUpdates) {
// 如果在一个 unbatched 更新块中,继续执行工作
performWorkOnRoot(root, expirationTime);
}
return;
}
// 根据 expirationTime 请求实际的浏览器调度
requestHostCallback(() => {
performWorkUntilDeadline(); // 当浏览器回调发生时,开始执行工作
});
}
// 当浏览器回调被调用时,执行此函数
function performWorkUntilDeadline() {
// ... 一些检查和设置
// 进行实际的工作
while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
// ... 更新 nextFlushedRoot 和 nextFlushedExpirationTime
}
// ... 判断是否还有更多工作要做,并可能再次请求 host callback
}
// 请求浏览器调度的实际方法。在 React 中,这通常使用 requestIdleCallback 或 setTimeout 实现,
// 取决于浏览器支持和任务优先级。
function requestHostCallback(callback) {
// ... 使用 requestIdleCallback 或 setTimeout 来调度 callback
}
注意:上述代码是简化和概括的版本,真正的 React
源代码中 requestWork
以及相关函数包含了大量的细节和优化策略。此外,React 还涉及到与浏览器的集成,例如通过 requestIdleCallback
来获取浏览器的空闲时间,这使得 React
可以在这些空闲时段中处理低优先级的工作。
performWorkOnRoot
- 执行工作 :
performWorkOnRoot
是真正开始工作的地方,这里会处理更新并执行渲染。
performWorkOnRoot
是 React 内部调度机制的核心部分之一,它是开始对某个 root 进行实际工作的地方。根据给定的 expirationTime
,它会决定何时开始工作、何时提交工作,以及如何利用浏览器的空闲时间。
js
function performWorkOnRoot(root, expirationTime) {
// 记录当前正在处理的 root 和 expirationTime
currentlyProcessingRoot = root;
renderExpirationTime = expirationTime;
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// 如果已经有完成的工作,直接进行提交
completeRoot(root, finishedWork, expirationTime);
} else {
// 从 root 的 current fiber 开始工作
workInProgress = createWorkInProgress(root.current, null);
// 开始进入渲染的工作循环
renderRoot();
if (workInProgressRootExitStatus === Finished) {
// 如果工作已完成,将 finishedWork 指向完成的工作,然后提交
root.finishedWork = workInProgress;
completeRoot(root, workInProgress, expirationTime);
}
}
}
function renderRoot() {
let work = workInProgress;
do {
// 执行单个 fiber 的更新工作
const result = performUnitOfWork(work);
if (result !== null) {
// 如果返回了下一个要处理的 fiber,继续执行
work = result;
} else {
// 如果当前 fiber 的子任务已经完成,进入完成阶段
const returnFiber = work.return;
const siblingFiber = work.sibling;
// 为父 fiber 完成工作
completeUnitOfWork(work);
if (siblingFiber !== null) {
// 如果有同级的 fiber,处理它
work = siblingFiber;
} else if (returnFiber !== null) {
// 如果有父 fiber,返回并处理它
work = returnFiber;
} else {
// 如果没有要处理的 fiber,退出循环
workInProgressRootExitStatus = Finished;
return;
}
}
} while (work !== null && shouldYieldToRenderer()); // shouldYieldToRenderer 判断是否应当暂停工作,让浏览器执行其他任务
}
function completeRoot(root, finishedWork, expirationTime) {
// 在这里,React 会执行真正的 DOM 更新操作,应用所有的 side effects
commitRoot(root, finishedWork);
}
这只是 performWorkOnRoot
的一个概览。在实际的 React 源代码中,该函数会涉及大量的优化和其他功能,如错误边界处理、Suspense、并发模式、事件处理等。
workLoop
- 工作循环:在执行工作时,React 会使用一个工作循环,逐个处理 Fiber 节点。
在 React 的 Fiber 架构中,工作循环 (work loop) 是核心的一部分。它决定何时执行某个 Fiber
对象的渲染工作,何时暂停来让浏览器执行其他高优先级任务,以及何时继续或完成工作。
js
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
// 如果有下一个工作单元并且当前不需要让出控制权给浏览器,继续工作
while (nextUnitOfWork !== null && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork === null) {
// 如果没有更多工作要做,可能提交工作
commitAllWork();
} else {
// 如果还有更多工作要做,再次请求浏览器的空闲回调
requestIdleCallback(workLoop);
}
}
function performUnitOfWork(fiber) {
// 开始该 fiber 的工作。这包括渲染组件或更新 DOM。
beginWork(fiber);
if (fiber.child) {
// 如果 fiber 有子项,返回它,因为我们首先处理子项
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
// 当子项完成时,我们"完成"每个父项
completeUnitOfWork(nextFiber);
if (nextFiber.sibling) {
// 如果有同级的 fiber,返回并处理它
return nextFiber.sibling;
}
// 否则,我们继续完成父 fiber,并往上查找
nextFiber = nextFiber.return;
}
return null;
}
requestIdleCallback(workLoop);
- 工作循环 (workLoop) : 这是整个过程的驱动力。它在浏览器的空闲时间进行,确保用户交互等高优先级任务不会被阻塞。
- 单元工作 (performUnitOfWork) : 这是每个 Fiber 对象的实际工作。对于组件,这可能意味着运行组件的
render
方法,而对于 DOM 节点,这可能意味着实际更新 DOM。 - 开始工作 (beginWork) 和 完成工作 (completeUnitOfWork) : 这些表示 fiber 的两个阶段。在开始阶段,组件会被渲染。在完成阶段,side-effects 会被收集并预备提交。
上面只是简单的简化版,详细需要看 Github 代码,深入了解过程
commitRoot
- 提交阶段:完成所有工作后,进入提交阶段,此时会应用所有的 side effects 到 DOM。
commitRoot
是 React Fiber 架构中一个非常重要的部分,它负责将已完成的工作应用到 DOM 中。在此阶段,React 将执行 side-effects(例如真正的 DOM 更新、生命周期方法调用等)。
javascript
function commitRoot(root, finishedWork) {
// 1. 准备提交前的操作,如调用 getSnapshotBeforeUpdate
prepareForCommit(root.containerInfo);
// 2. 递归整个 effect list(包含所有带有 side-effect 的 fiber)
let nextEffect = finishedWork.firstEffect;
while (nextEffect !== null) {
// 根据 fiber 的 effect tag,执行相应的 side-effect
commitWork(nextEffect);
nextEffect = nextEffect.nextEffect;
}
// 3. 提交后的操作,如调用 componentDidUpdate 和 componentDidMount
completeAfterCommit(root);
// 4. 重置 nextEffect 和 effect list
finishedWork.firstEffect = finishedWork.lastEffect = null;
}
function commitWork(fiber) {
switch (fiber.effectTag) {
case PLACEMENT:
// 将新的节点插入 DOM
break;
case UPDATE:
// 更新 DOM 节点
break;
case DELETION:
// 从 DOM 中删除节点
break;
// ... 其他 effect tags 如 CALLBACK, REF, SNAPSHOT 等
}
// 清除 effect tag,表示 side-effects 已被处理
fiber.effectTag = NoEffect;
}
function prepareForCommit(containerInfo) {
// 在这里,React 会执行一些提交前的操作。
// 例如,它可能会调用类组件的 getSnapshotBeforeUpdate 生命周期方法。
}
function completeAfterCommit(root) {
// 在这里,React 会执行提交后的操作。
// 例如,它可能会调用 componentDidMount 和 componentDidUpdate 生命周期方法。
}
cleanUp
清理工作:完成所有的更新后,React 会清理所有不再需要的内容,如解除事件监听器、取消异步更新等。
当 React 完成更新后,它会进入提交阶段,其中一部分工作是清理旧的、不再需要的内容。这主要涉及到移除不再需要的 DOM 节点、取消未完成的工作、释放引用以便于垃圾收集等。
最明显的清理工作是处理带有 DELETION
effect tag 的 Fiber
对象。当某个组件被判定为不再需要时(例如,由于父组件的重新渲染导致它不再在输出中),该组件的 Fiber
将被标记为 DELETION
。
js
function commitDeletion(fiber) {
// 移除该 fiber 及其子树中的节点
unmountFiberRecursively(fiber);
detachFiber(fiber);
}
function unmountFiberRecursively(fiber) {
// 1. 如果 fiber 有子组件,递归删除
let child = fiber.child;
while (child) {
unmountFiberRecursively(child);
child = child.sibling;
}
// 2. 对当前 fiber 执行卸载操作
unmountFiber(fiber);
}
function unmountFiber(fiber) {
// 清理具体的工作,如移除 DOM 节点、调用 componentWillUnmount 生命周期方法等
switch (fiber.tag) {
case HostComponent: // DOM 节点
commitNestedUnmounts(fiber.stateNode);
return;
case ClassComponent:
// 如果是类组件,可能需要调用 componentWillUnmount
if (typeof fiber.type.prototype.componentWillUnmount === 'function') {
fiber.type.prototype.componentWillUnmount.call(fiber.stateNode);
}
return;
// ...处理其他类型的 fiber
}
}
function detachFiber(fiber) {
// 这里会释放 fiber 对象的引用,使得它能够被垃圾收集
fiber.alternate = null;
fiber.child = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.return = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;
}
总结
React 的调度机制(Scheduler)是其并发模式的核心,它决定何时开始或中断工作,以确保在保持应用响应的同时高效地进行更新。这个概述是基于 React 16+ 中的 Fiber 架构,特别是它在并发模式中的工作方式。
requestWork
,performWorkOnRoot
,workLoop
步骤 是 React Scheduler
的核心部分。通过在浏览器的空闲时间进行工作,React 确保了应用保持响应,并在需要的时候可以中断和重新开始工作。
如果我在上述的内容存在问题欢迎在评论区留下你宝贵的建议,希望这个对你有帮助,互相学习进步。谢谢大家
文章参考: