是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情”
【学习笔记】React18源码学习(二)Reconciler
1. React哲学
我们认为,React 是用 JavaScript 构建
快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。
制约快速响应的两个瓶颈 CPU 和 IO
- CPU
浏览器60Hz,所以是1000ms/60=16.6ms 浏览器刷新一次,在这个刷新期间浏览器会执行
graph LR
JS脚本执行 --> 样式布局 --> 样式绘制
如果js脚本执行超过16ms 就会阻塞后面的布局,就会造成页面的卡顿。
react解决方案:将同步更新变为异步可中断的更新
- IO
react 将人机交互的研究成果整合到真实的UI中
2. 新老架构
老的React架构 react15:
graph LR
Reconcile协调器--> Render渲染器
老的react架构为同步的更新,递归执行,数据保存在调用栈中被称为Stack Reconciler
新的Fiber架构架构
graph LR
Scheduler调度器 --> Reconciler协调器 --> Render渲染器
新的架构调度器负责决定更新的优先级,协调器负责创建Fiber,和生成FiberList,渲染器负责将fiberList渲染为真实的DOM,所以被称为Fiber Reconciler
React为实现异步可中断的更新,使用Fiber(纤程)。 Fiber架构有两个特性
- 更新可以中断并有空余的时间继续
- 高优先级的更新可以中断低优先级的更新\
Fiber的三层含义
- 作为架构
Fiber Reconciler - 作为静态的数据结构
虚拟DOM - 作为动态的工作单元
需要更新的状态和需要执行的副作用
// FiberNode
// Instance
this.tag = tag; // fiber类型 比如div就是Host Component
this.key = key; // 键 列表的标记
this.elementType = null;
this.type = null; // 类型 对于functionComponent 是 function,对于Class Component 是 构造函数,对于Host Component 是 TagName
this.stateNode = null; // 对于Host Component 是 DOM对应的真实节点
// Fiber
this.return = null; // 将节点连接在一起
this.child = null; // 子节点
this.sibling = null; // 兄弟节点
this.index = 0; // 多个同级节点插入DOM的位置
this.ref = null; // 同Ref
this.refCleanup = null; //
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // 副作用
this.subtreeFlags = NoFlags; // 子节点通过冒泡到当前节点的副作用
this.deletions = null;
this.lanes = NoLanes; // 优先级
this.childLanes = NoLanes; //子节点的优先级
this.alternate = null; // current fiber指向work in progress fiber;working in progress fiber指向current fiber
双缓存与Fiber
概念:在内存中构建两个Fiber树,一棵叫CurrentFiber,WorkInProgreesFiber,解决卡顿
Demo
function App() {
const [num,add] = useState(0)
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
- ReactDOM.createRoot 会创建整个应用的根节点FiberRootNode
- root.render 创建应用的子节点RootFiber
graph TD
A[FiberRootNode] -- current --> B[RootFiber]
- 进入首屏渲染的逻辑
- 创建一棵
WorkInProgressFiber树,第一个节点为RootFiber,由于currentFiber已存在所以会用alternate属性连接,方便两个节点 - 将剩余节点用深度优先遍历创建整棵fiber树
- 创建完成后将
current指向WorkInProgressFiber树 - 更新逻辑
- 点击P标签,触发更新,用jsx 与
Current Fiber使用Diff算法做对比(首屏渲染与触发更新的区别),生成WorkInProgressFiber,构建完成后将current指向
3. Scheduler调度器
脚踏N条船的时间管理大师
跑起来代码,打开chrome,打开控制台,打开Performance,点击录制,点暂停,看到时间轴里蓝色的标签位置也就是DOMContentLoaded Event,放大再放大。
可以看到调度器阶段依次执行的四个方法,接下来就逐个分析
- performWorkUntilDeadline
debug一下会发现call stack里面从index.js开始执行
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
在 React 18 中,ReactDOM.createRoot 已经被废弃,取而代之的是 ReactDOM.createRootContainer 方法。不过它们的原理都是一样的。
ReactDOMRoot.prototype.render()
root.render 会调用 ReactDOMRoot.prototype.render()
function (children) {
// 这里的root 就是上面真实的DOM的root节点
var root = this._internalRoot;
updateContainer(children, root, null, null);
};
updateContainer
// 删掉了一些 devtools 和 性能分析 相关不用考虑的代码
function updateContainer(element, container, parentComponent, callback) {
const current = container.current; // FiberRootNoe
const eventTime = requestEventTime(); //当前事件执行的时间
// 优先级为32
// 查看ReactFiberLane.js 为 DefaultLane
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
update.payload = {element};
/**
👆两步执行结果
update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: element,
callback: null,
next: null
};
**/
// 入队更新
const root = enqueueUpdate(current, update, lane);
// 生成FiberRootNode 且 current 指向了 RootFiber
if (root !== null) {
// 调度更新
scheduleUpdateOnFiber(root, current, lane, eventTime);
// 处理在更新期间可能出现的暂停和恢复操作
entangleTransitions(root, current, lane);
}
return lane;
}
调用 enqueueUpdate 函数将 update 添加到当前 Fiber 节点的更新队列中,并传递 current 表示当前 Fiber 节点。如果当前 Fiber 节点没有更新队列,那么会创建一个新的更新队列。
之后,返回的 root 指针指向的是当前 Fiber 节点的更新队列中的最后一个更新对象。
这个更新对象包含了最新的更新内容。同时,这个更新对象还会记录一些元数据,例如哪些更新 lane(优先级)需要更新、哪些更新需要被合并等。
通过entangleTransitions这个函数会将 current Fiber 节点的暂停操作和 root 的恢复操作添加到一个双向链表中,并将它们绑定在一起。这个链表称为FiberRoot entanglements链表。在 scheduleCallbackForRoot 函数中,React Fiber 架构会遍历这个链表,并执行“暂停”操作和“恢复”操作。
当 entangleTransitions 函数被调用时,它会检查当前的 Fiber 节点是否已经被暂停,如果是,则会将当前节点的“暂停”操作添加到 root 的“恢复”操作上。如果当前节点没有被暂停,那么不需要执行任何操作。
scheduleUpdateOnFiber
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// 标记root的更新
markRootUpdated(root, lane, eventTime);
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
root:表示该组件所在的 Fiber 树的根节点(即FiberRoot对象)。fiber:表示要更新的组件对应的 Fiber 节点。lane:表示更新的优先级。eventTime:表示更新的时间戳。
在 scheduleUpdateOnFiber 函数中,首先会调用 markUpdateLaneFromFiberToRoot 函数,该函数的作用是将更新的优先级 lane 标记在从当前 fiber 节点到根节点 root 的所有节点上。
接下来,会调用 ensureRootIsScheduled为了确保组件对应的 Fiber 树能够被及时地进行更新,需要注意的是ensureRootIsScheduled并不会立即执行更新操作,而是将组件对应的 Fiber 树加入到调度器中,等待后续调度器进行调度,以便在适当的时间执行更新操作
ensureRootIsScheduled
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
schedulerPriorityLevel是一个枚举值,表示回调函数的优先级。
React 中定义了几个优先级常量,包括 ImmediatePriority、UserBlockingPriority、NormalPriority、LowPriority、IdlePriority 等。
不同的优先级对应不同的操作,例如 UserBlockingPriority 用于处理用户交互事件,LowPriority 用于执行一些低优先级的操作。
这里的优先级是NormalPriority
- callback
performConcurrentWorkOnRoot.bind(null, root),第一个参数 null 表示回调函数中的 this 值为全局对象,scheduleCallback 函数返回一个 callbackNode 对象,用于标识和取消回调函数,返回的 callbackNode 赋值给了 newCallbackNode 变量,表示新创建的回调函数的标识。可以通过这个标识,随时取消回调函数的执行。
flushWork
function flushWork(hasTimeRemaining: boolean, initialTime: number) {
// We'll need a host callback the next time work is scheduled.
const previousPriorityLevel = currentPriorityLevel;
try {
// 执行workLoop 这里就是开始render阶段了
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
4.承上启下调度器与协调器的过渡
WorkLoop
/**
@param hasTimeRemaining 剩余时间
@param initialTime 初始时间
**/
function workLoop(hasTimeRemaining: boolean, initialTime: number) {
let currentTime = initialTime;
// 检查不再延迟的任务并将其添加到队列中。
advanceTimers(currentTime);
// 从taskQueue中取出一个任务
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 当前任务过期
break;
}
// 当前 callBack 是 performConcurrentWorkOnRoot(root, didTimeout) {
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// BreakPoint
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 如果返回了一个continuation,则立即让位给主线程 而不管当前时间片中还剩多少时间。
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
return true;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
重点在这里
const continuationCallback = callback(didUserCallbackTimeout);
这里执行的callBack 方法来源于 ensureRootIsScheduled的最后一行
newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
所以执行的是这个performConcurrentWorkOnRoot。
performConcurrentWorkOnRoot
function performConcurrentWorkOnRoot(root: FiberRoot, didTimeout: boolean) {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw '当前不应处于React工作流程内';
}
// 开始执行具体工作前,保证上一次的useEffct都执行了
// 同时要注意useEffect执行时触发的更新优先级是否大于当前更新的优先级
const didFlushPassiveEffects = flushPassiveEffects(
root.pendingPassiveEffects
);
const curCallbackNode = root.callbackNode;
if (didFlushPassiveEffects) {
if (root.callbackNode !== curCallbackNode) {
// 调度了更高优更新,这个更新已经被取消了
return null;
}
}
const lanes = getNextLanes(root);
if (lanes === NoLanes) {
return null;
}
// 本次更新是否是并发更新?
// TODO 饥饿问题也会影响shouldTimeSlice
const shouldTimeSlice = !didTimeout;
const exitStatus = renderRoot(root, lanes, shouldTimeSlice);
ensureRootIsScheduled(root);
if (exitStatus === RootIncomplete) {
if (root.callbackNode !== curCallbackNode) {
// 调度了更高优更新,这个更新已经被取消了
return null;
}
return performConcurrentWorkOnRoot.bind(null, root);
}
// 各种边界的 if else。。。省略
// 现在我们有了一个一致的树形结构。下一步是要提交它
// 或者如果有什么被暂停了,就等待一段时间后再提交它。
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
// 首先,使用 `ensureRootIsScheduled` 函数来确保当前的根节点被调度。
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
if (
workInProgressSuspendedReason === SuspendedOnData &&
workInProgressRoot === root
) {
// 工作循环当前处于暂停状态并等待数据解析。
// 在这种情况下,需要取消当前任务,以便在数据解析完成后重新调度
root.callbackPriority = NoLane;
root.callbackNode = null;
return null;
}
// 如果没有特殊情况需要处理,则返回一个函数
// 绑定了 `root` 参数,它用于执行并发模式下的工作循环。
// 它会递归遍历 fiber 节点树,执行节点上的任务,直到所有的任务都执行完成。
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}