写在前面
本系列会实现一个简单的react
,包含最基础的首次渲染,更新,hook
,lane
模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的并发更新流程,代码均已上传至github
,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来…😭😭
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
在前面已经介绍了整个scheduler
的调度机制,那么在react中怎么将调度机制加入到更新任务中的呢?
首先如果在更新任务中加入调度机制,那么需要定义一些任务执行的优先级,由于scheduler
是一个独立的包,只对传入的任务进行调度执行,不会涉及到真正的业务代码的执行,所以在react中需要定义任务执行的优先级:
// 同步优先级 最高优先级
export const SyncLane = 0b0001;
// input输入事件等 优先级
export const InputContinuousLane = 0b0010;
// 普通优先级
export const DefaultLane = 0b0100;
// 空闲
export const IdleLane = 0b1000;
接下来分别从初始化和更新两个方面来看一下整个分配优先级调度任务的流程。
mount
为了能够更加丝滑的带入本文,首先来回顾一下之前实现的初始化流程:
mount
阶段由下面这段代码开始触发:
ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
createRoot
函数创建应用根节点和fiber
树的根节点,然后传入函数组件<App />
开始构建fiber
树。为整棵fiber
树的事件进行事件委托。
随后创建一个更新对象,放入根节点的更新队列。随后在workLoop
中进入beginWork
流程,开始根据fiber
节点的next
属性根据节点的不同类型,创建子级fiber
节点。
在所有子节点都创建完成后,开始completeWork
流程,完成收集副作用,创建真实DOM节点等操作。在回溯到根节点后,此时一整棵fiber
树就创建完成了,然后根据双缓存机制切换fiber
树。开启commit
流程,挂载fiber
节点中的stateNode
属性(真实DOM节点)到页面中。
在mount
初始化时,updateContainer
函数中首先要创建一个更新对象,所以在此之前我们要为这个任务设置优先级,便于在该更新对象中进行保存。
- 获取当前触发更新的上下文的优先级
- 根据优先级创建更新对象,并保存到根节点的更新队列中
- 开始调度
export function updateContainer(element, root) {
const hostRootFiber = root.current;
// 获取当前上下文优先级
++ const lane = requestUpdateLane();
// 创建update更新对象
++ const update = createUpdate(element, lane);
// 传入lane,代表本次更新任务优先级
-- const update = createUpdate(element);
enqueueUpdate(hostRootFiber.updateQueue, update);
// 调度更新任务
scheduleUpdateOnFiber(hostRootFiber, lane);
return element;
}
requestUpdateLane
获取优先级,unstable_getCurrentPriorityLevel
是scheduler
中内置的获取优先级的方法,默认返回一个普通优先级:
var currentPriorityLevel = NormalPriority;
function unstable_getCurrentPriorityLevel(): PriorityLevel {
return currentPriorityLevel;
}
由于在scheduler
中设置的优先级与我们实现的react中的优先级虽然级别和功能一致,但是名称完全不同:
// scheduler中的优先级
export const NoPriority = 0; // 对应react中的NoLane
export const ImmediatePriority = 1; // 对应react中的SyncLane
export const UserBlockingPriority = 2; // 对应react中的InputContinuousLane
export const NormalPriority = 3; // 对应react中的DefaultLane
export const IdlePriority = 5; // 对应react中的IdleLane
所以在获取到优先级后,还需要与react中设置的优先级进行转换:
export function schedulerPriorityToLane(schedulerPriority) {
if (schedulerPriority === unstable_ImmediatePriority) {
return SyncLane;
}
if (schedulerPriority === unstable_UserBlockingPriority) {
return InputContinuousLane;
}
if (schedulerPriority === unstable_NormalPriority) {
return DefaultLane;
}
return NoLane;
}
import { unstable_getCurrentPriorityLevel } from 'scheduler';
export function requestUpdateLane() {
// 从上下文环境中获取Scheduler优先级
const currentSchedulerPriority = unstable_getCurrentPriorityLevel();
const lane = schedulerPriorityToLane(currentSchedulerPriority);
return lane;
}
在创建更新对象时,也要对优先级进行保存:
export const createUpdate = (action, lane) => {
return {
action,
// 本次更新的优先级
lane,
// 下一个更新任务
next: null
};
};
最后使用enqueueUpdate
函数将本次更新产生的更新对象保存在更新队列中(shared.pending
),采用环形链表的方式保存(上一篇文章中有详细介绍)。
接下来开始对更新任务进行调度。
scheduleUpdateOnFiber
scheduleUpdateOnFiber
作为开始调度任务的入口,掌管着所有更新任务的流转和执行。无论是mount
还是update
阶段,在生成更新对象后都会进入scheduleUpdateOnFiber
函数来对任务进行调度。
由于在更新阶段可能触发更新的动作位于任何一个fiber
节点上,但是react需要在更新时生成一个全新的fiber
树,所以在进入scheduleUpdateOnFiber
时,首先要从触发更新的目标节点开始,向上查找到根节点,然后从根节点开始构造一颗新的fiber
树(workInProgress
树)。这个过程前面的文章已经重复说过很多遍了,不再赘述。
// 查找根节点
const root = markUpdateFromFiberToRoot(fiber);
// 根据return属性不断向上寻找
function markUpdateFromFiberToRoot(fiber) {
let node = fiber;
let parent = node.return;
while (parent !== null) {
node = parent;
parent = node.return;
}
if (node.tag === HostRoot) {
return node.stateNode;
}
return null;
}
然后将此次更新的优先级保存到根节点中,因为每次更新都是从根节点开始,所以保存在根节点中便于统一处理当前产生的所有不同类型的优先级任务。
markRootUpdated(root, lane);
function markRootUpdated(root, lane) {
// 统一合并到根节点的pendingLanes属性上
root.pendingLanes = mergeLanes(root.pendingLanes, lane);
}
export function mergeLanes(laneA, laneB) {
return laneA | laneB;
}
最后开始统一在根节点上根据优先级调度任务:
export function scheduleUpdateOnFiber(fiber, lane) {
// 查找根节点
const root = markUpdateFromFiberToRoot(fiber);
// 合并优先级lane
++ markRootUpdated(root, lane);
// 开始调度
ensureRootIsScheduled(root);
}
ensureRootIsScheduled
ensureRootIsScheduled
函数正式开始一次更新任务触发的调度,整个过程会涉及到筛选高优先级任务,任务取消,高优先级任务插队打断低优先级任务的过程。
由于上篇文章 scheduler调度机制原理 已经非常详细的讲解了整个scheduler
的调度过程,所以在本文中只针对react涉及到的任务调度过程,实现使用scheduler
暴漏的方法完成对react更新任务的调度功能。
-
在
root
(根节点)节点中的pendingLanes
属性中取出最高优先级 -
如果
pendingLanes
中没有优先级,说明无需执行任务a.当前正在执行任务,直接取消正在执行的任务(说明当前已经不需要执行任何调度任务)
-
如果当前获取到的本次更新优先级与正在执行的任务优先级相同,不调度新任务,继续执行原任务
-
如果当前获取到的本次更新优先级大于正在执行任务的优先级,取消原任务
-
本次更新是同步优先级(最高优先级)?
a. 同步优先级使用微任务调度
b. 其他优先级使用宏任务调度
function ensureRootIsScheduled(root: FiberRootNode) {
// 获取最高优先级
const updateLane = getHighestPriorityLane(root.pendingLanes);
// 优先级不存在
if (updateLane === NoLane) {
// 如果还有正在执行的任务
// 取消掉
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
return;
}
const curPriority = updateLane;
const prevPriority = root.callbackPriority;
// 本次更新任务与正在执行的任务是同一个优先级
// 继续执行,不打断当前的任务
if (curPriority === prevPriority) {
// 不调度新任务
return;
}
// 本次更新任务优先级高于当前正在执行的任务优先级
// 取消当前正在执行的任务
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
let newCallbackNode = null;
if (updateLane === SyncLane) {
// 同步优先级 用微任务调度
// ...省略
} else {
// 其他优先级 用宏任务调度
// ...省略
}
// ...省略
}
其中unstable_cancelCallback
是scheduler
中提供的取消任务的方法,具体可见上一篇文章。
这里会有一个问题,如果将当前低优先级的任务取消掉,在scheduler
的unstable_cancelCallback
会直接将执行函数赋值为null,那么是不是这个低优先级的任务后续永远都得不到执行呢?
其实低优先级任务被取消后后,它的优先级并没有被从root.pendingLanes
中移出去,当高优先级任务做完后(commit
阶段的最后),会调用ensureRootIsScheduled
函数,这个函数中会检查root.pendingLanes
中是否还有未做的优先级。这时候发现了有之前被取消的低优先级任务的优先级,会再发起一次全新的调度,把这个低优先级任务执行掉。
获取最高优先级:
export function getHighestPriorityLane(lanes) {
return lanes & -lanes;
}
如何判断当前正在执行的任务呢?在任务开始时会将当前的任务保存在root
节点的属性callbackNode
中,在任务执行完毕后重置此属性,如果callbackNode
属性有值,说明本次更新触发时有任务还在执行中。
// 获取callbackNode属性
const existingCallback = root.callbackNode;
// ...省略
// 任务开始执行
// 新的更新任务被调度执行
root.callbackNode = newCallbackNode;
root.callbackPriority = curPriority;
任务的两种调度方式,其实在lane
那一篇文章也已经说过一部分了:
if (updateLane === SyncLane) {
// 同步优先级 用微任务调度
// [performSyncWorkOnRoot, performSyncWorkOnRoot, performSyncWorkOnRoot]
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
// 开启微任务调度
scheduleMicroTask(flushSyncCallbacks);
} else {
// 其他优先级 用宏任务调度
const schedulerPriority = lanesToSchedulerPriority(updateLane);
newCallbackNode = scheduleCallback(
schedulerPriority,
performConcurrentWorkOnRoot.bind(null, root)
);
}
当本次任务为同步优先级时,scheduleSyncCallback
负责收集执行函数,并且将根节点和优先级当作参数传入。scheduleMicroTask
调用微任务,flushSyncCallbacks
为具体执行的函数。
export function scheduleSyncCallback(callback) {
if (syncQueue === null) {
syncQueue = [callback];
} else {
syncQueue.push(callback);
}
}
export const scheduleMicroTask =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof Promise === 'function'
? (callback: (...args: any) => void) => Promise.resolve(null).then(callback)
: setTimeout;
export function flushSyncCallbacks() {
if (!isFlushingSyncQueue && syncQueue) {
isFlushingSyncQueue = true;
try {
syncQueue.forEach((callback) => callback());
} catch (e) {
if (__DEV__) {
console.error('flushSyncCallbacks报错', e);
}
} finally {
isFlushingSyncQueue = false;
syncQueue = null;
}
}
}
如果本次更新是其他非同步任务,则使用scheduler
中的调度函数unstable_scheduleCallback
执行。不过在此之前由于我们目前使用的是react的优先级,所以在调用unstable_scheduleCallback
之前要进行优先级转换:
export function lanesToSchedulerPriority(lanes) {
const lane = getHighestPriorityLane(lanes);
if (lane === SyncLane) {
return unstable_ImmediatePriority;
}
if (lane === InputContinuousLane) {
return unstable_UserBlockingPriority;
}
if (lane === DefaultLane) {
return unstable_NormalPriority;
}
return unstable_IdlePriority;
}
unstable_scheduleCallback
方法接收一个优先级和对应任务函数:
newCallbackNode = unstable_scheduleCallback(
schedulerPriority,
performConcurrentWorkOnRoot.bind(null, root)
);
ensureRootIsScheduled
完整代码:
function ensureRootIsScheduled(root) {
// 获取最高优先级
const updateLane = getHighestPriorityLane(root.pendingLanes);
// 获取callbackNode用来判断当前是否有任务正在执行
const existingCallback = root.callbackNode;
// 优先级不存在
if (updateLane === NoLane) {
// 如果还有正在执行中的任务
// 取消掉
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
// 重置
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
const curPriority = updateLane;
const prevPriority = root.callbackPriority;
// 本次更新任务与正在执行的任务是同一个优先级
// 继续执行,不打断当前的任务
if (curPriority === prevPriority) {
return;
}
// 本次更新任务优先级高于当前正在执行的任务优先级
// 取消当前任务
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
let newCallbackNode = null;
if (updateLane === SyncLane) {
// 同步优先级,使用微任务执行
// [performSyncWorkOnRoot, performSyncWorkOnRoot, performSyncWorkOnRoot]
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
scheduleMicroTask(flushSyncCallbacks);
} else {
// 其他优先级 使用unstable_scheduleCallback调度
const schedulerPriority = lanesToSchedulerPriority(updateLane);
newCallbackNode = scheduleCallback(
schedulerPriority,
performConcurrentWorkOnRoot.bind(null, root)
);
}
// 保存当前任务函数和优先级
root.callbackNode = newCallbackNode;
root.callbackPriority = curPriority;
}
performSyncWorkOnRoot 与 performConcurrentWorkOnRoot
performConcurrentWorkOnRoot
由于涉及到采用并发更新,所以一个任务调度执行涉及到两种情况:
- 暂时中断
- 执行完毕
当任务并发更新时,如果任务只是被暂时中断,并未完全执行完(时间片切断执行/高优先级任务打断),在scheduler
中会继续调用执行结果(参考上一篇scheduler
调度原理)。这也就是说当任务被暂时中断时我们还需要返回自身函数,由
scheduler
继续调用。
所以需要两个标志,标记任务是否执行完毕:
// 任务未执行完
const RootInComplete = 1;
// 任务已执行完
const RootCompleted = 2;
当scheduler
开始真正执行performConcurrentWorkOnRoot
函数时,会根据返回值判断任务时候执行完毕,如果performConcurrentWorkOnRoot
返回一个函数,那么scheduler
会继续调用此函数,直到完全执行完毕。
renderRoot
在render
阶段会返回一个标识,RootInComplete
或者RootCompleted
,用于判断任务是否执行完毕。
function performConcurrentWorkOnRoot(root, didTimeout) {
const lane = getHighestPriorityLane(root.pendingLanes);
const curCallbackNode = root.callbackNode;
if (lane === NoLane) {
return null;
}
const needSync = lane === SyncLane || didTimeout;
// render阶段返回值
const exitStatus = renderRoot(root, lane, !needSync);
// 再次调度
ensureRootIsScheduled(root);
// 任务是被强行中断的
if (exitStatus === RootInComplete) {
// 中断
if (root.callbackNode !== curCallbackNode) {
return null;
}
// 返回自身函数交给unstable_scheduleCallback继续调用被中断的任务
return performConcurrentWorkOnRoot.bind(null, root);
}
// 任务完成
// 重置属性,开始commit阶段
if (exitStatus === RootCompleted) {
// ...省略
commitRoot(root);
} else if (__DEV__) {
console.error('还未实现的并发更新结束状态');
}
}
renderRoot
根据workInProgress
判断是否执行完render
阶段,workInProgress
在执行render
阶段时会一直被赋值为下一个待处理的节点。shouldTimeSlice
代表是否开启时间切片,也就是根据一定的时间间隔暂停任务。
function renderRoot(root, lane, shouldTimeSlice) {
if (wipRootRenderLane !== lane) {
// 初始化
prepareFreshStack(root, lane);
}
do {
try {
// 以并发/同步执行workLoop函数
shouldTimeSlice ? workLoopConcurrent() : workLoopSync();
break;
} catch (e) {
}
} while (true);
// 中断执行
if (shouldTimeSlice && workInProgress !== null) {
return RootInComplete;
}
// render阶段执行完
if (!shouldTimeSlice && workInProgress !== null && __DEV__) {
console.error(`render阶段结束时wip不应该不是null`);
}
return RootCompleted;
}
workLoopConcurrent
和workLoopSync
代替原有的workLoop
函数开始render
阶段,两者的区别是,当并发更新时会有一个unstable_shouldYield
函数控制执行,这个方法是scheduler
中提供的方法,以一定的时间间隔返回true
或者false
,当超过规定的时间间隔会控制workLoopConcurrent
暂停继续执行。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !unstable_shouldYield()) {
performUnitOfWork(workInProgress);
}
}
performSyncWorkOnRoot
同步更新不会涉及任务的暂停,会一直以同步的方式执行完,由于在调用时会在微任务的时机中执行,所以连续触发的更新也会合并。
function performSyncWorkOnRoot(root) {
const nextLane = getHighestPriorityLane(root.pendingLanes);
if (nextLane !== SyncLane) {
// 其他比SyncLane低的优先级
ensureRootIsScheduled(root);
return;
}
// 获取返回值
const exitStatus = renderRoot(root, nextLane, false);
if (exitStatus === RootCompleted) {
// ...重置属性
// wip fiberNode树 树中的flags
commitRoot(root);
} else if (__DEV__) {
console.error('还未实现的同步更新结束状态');
}
}
update
由于开启并发更新后,无论是优先级打断还是时间片打断,在更新时的状态可能不会按照原有的更新顺序执行,执行的结果也会有差异。比如如果一个低优先级触发的计算逻辑被中断,高优先级的计算逻辑会被先计算,这也就会和同步更新的计算结果有差异,对这种情况也要在更新阶段进行处理。
在前面在保存更新任务时,我们使用了环形链表的形式进行存储:
// 多个更新任务
pending = b -> a -> b
pending = c -> a -> b -> c
在开始执行状态更新时,由于fiber
节点上的更新任务是由环形链表存储,所以需要遍历每个更新任务,寻找与本次调度任务优先级一致的更新任务(update
对象)来更新状态,这也就会导致某些更新任务会被跳过:
跳过update需要考虑的问题
如何比较 「优先级是否足够」 ?
lane
数值大小的直接比较不够灵活。
如何同时兼顾 「update的连续性」 与 「update的优先级」 ?
// u0
{
action: num => num + 1,
lane: DefaultLane
}
// u1
{
action: 3,
lane: SyncLane
}
// u2
{
action: num => num + 10,
lane: DefaultLane
}
// state = 0; updateLane = DefaultLane
// 只考虑优先级情况下的结果:11
// 只考虑连续性情况下的结果:13
新增baseState
、baseQueue
字段:
-
baseState
是本次更新参与计算的初始state
,memoizedState
是上次更新计算的最终state
- 如果本次更新没有
update
被跳过,则下次更新开始时baseState
===memoizedState
- 如果本次更新有
update
被跳过,则本次更新计算出的memoizedState
为 「考虑优先级」 情况下计算的结果,baseState
为 「最后一个没被跳过的update计算后的结果」 ,下次更新开始时baseState
!==memoizedState
- 本次更新 「被跳过的update及其后面的所有update」 都会被保存在
baseQueue
中参与下次state
计算
// u0
{
action: num => num + 1,
lane: DefaultLane
}
// u1
{
action: 3,
lane: SyncLane
}
// u2
{
action: num => num + 10,
lane: DefaultLane
}
/*
* 第一次render
* baseState = 0; memoizedState = 0;
* baseQueue = null; updateLane = DefaultLane;
* 第一次render 第一次计算
* baseState = 1; memoizedState = 1;
* baseQueue = null;
* 第一次render 第二次计算
* baseState = 1; memoizedState = 1;
* baseQueue = u1;
* 第一次render 第三次计算
* baseState = 1; memoizedState = 11;
* baseQueue = u1 -> u2(NoLane);
*/
/*
* 第二次render
* baseState = 1; memoizedState = 11;
* baseQueue = u1 -> u2(NoLane); updateLane = SyncLane
* 第二次render 第一次计算
* baseState = 3; memoizedState = 3;
* 第二次render 第二次计算
* baseState = 13; memoizedState = 13;
*/
从第一个与调度任务优先级不匹配的更新任务开始,重新构建一条新的链表,当作此fiber
新的shared.pending
的值(更新任务链表)。
processUpdateQueue
函数在计算更新函数结果时触发,参见前面初始化及更新流程的文章。
export const processUpdateQueue = (
baseState,
pendingUpdate,
renderLane
) => {
// 定义处理结果
const result = {
memoizedState: baseState,
baseState,
baseQueue: null
};
if (pendingUpdate !== null) {
// 第一个update
const first = pendingUpdate.next;
let pending = pendingUpdate.next;
// 初始化
let newBaseState = baseState;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let newState = baseState;
do {
const updateLane = pending.lane;
// 判断当前更新任务优先级是否与当前调度任务一致
if (!isSubsetOfLanes(renderLane, updateLane)) {
// 优先级不够 被跳过
} else {
// 优先级足够
// 执行更新任务
const action = pending.action;
if (action instanceof Function) {
newState = action(baseState);
} else {
newState = action;
}
}
pending = pending.next;
} while (pending !== first);
}
return result;
};
export function isSubsetOfLanes(set, subset) {
return (set & subset) === subset;
}
此外还需要考虑更新计算的结果state
:
export const processUpdateQueue = (
baseState,
pendingUpdate,
renderLane
) => {
// 定义处理结果
const result = {
memoizedState: baseState,
baseState,
baseQueue: null
};
if (pendingUpdate !== null) {
// 第一个update
const first = pendingUpdate.next;
let pending = pendingUpdate.next;
// 初始化
// 保存基础state
let newBaseState = baseState;
// 第一个跳过后新链表
let newBaseQueueFirst = null;
// 最后一个跳过后新链表
let newBaseQueueLast = null;
// 计算后新state
let newState = baseState;
do {
const updateLane = pending.lane;
// 判断当前更新任务优先级是否与当前调度任务一致
if (!isSubsetOfLanes(renderLane, updateLane)) {
// 优先级不够 被跳过
const clone = createUpdate(pending.action, pending.lane);
// 是不是第一个被跳过的?
// 第一个被跳过的update收尾都是当前更新任务
if (newBaseQueueFirst === null) {
newBaseQueueFirst = clone;
newBaseQueueLast = clone;
newBaseState = newState;
} else {
// 连接链表
newBaseQueueLast.next = clone;
newBaseQueueLast = clone;
}
} else {
// 优先级足够
// 如果之前已经有被跳过的更新任务,链接当前更新任务
if (newBaseQueueLast !== null) {
const clone = createUpdate(pending.action, NoLane);
newBaseQueueLast.next = clone;
newBaseQueueLast = clone;
}
// 执行更新任务
const action = pending.action;
if (action instanceof Function) {
newState = action(baseState);
} else {
newState = action;
}
}
pending = pending.next;
} while (pending !== first);
if (newBaseQueueLast === null) {
// 本次计算没有update被跳过
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
// 如果有任务被跳过,memoizedState保存包含高优先级的更新计算结果
result.memoizedState = newState;
// 如果有任务被跳过,baseState只包含最后一个未被跳过的更新任务计算结果
result.baseState = newBaseState;
// 从第一个跳过的更新任务开始组成的更新链表
result.baseQueue = newBaseQueueLast;
}
return result;
};
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳
原文链接:https://juejin.cn/post/7337578424721276966 作者:小黄瓜没有刺