说明#
- 本文基于 v18.1.0 进行分析。
- 在对于 useState 进行调试时会反复出现 interleaved 这一概念,请参考该 链接
- 文章大致上基于下面的代码进行分析,根据情况不同会有一定的改动。
- 2022-10-14 补充了关于批处理的源码分析(涉及 任务复用【相关】 与 高优先级插队【代码里有顺嘴一提】)与调度阶段的简单总结
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1) } }>{ text }</div>
)
}
export default UseState;
TLNR#
- useState 的 dispatch 是一个异步操作
- useState 的实现基于 useReducer,或者说 useState 就是一个特殊的 useReducer
- useState 每次调用其 dispatch 都会生成一个对应的 update 实例,多次调用同一个 dispatch 则会挂载成为一个 update 的链表。并且每个 update 都拥有一个对应的优先级(lane)
- 如果生成的 update 被判断为需要被调度更新,则会以 microTask 的形式被调度更新
- 在 useState 的 update 阶段内为了快速响应高优先级的 update,update 链表只会先处理高优先级的 update,低优先级的的 update 会在下一次 render 阶段内被调用更新。
- 批处理与(调度 → 协调 → 渲染)内的调度阶段总结请移步文末
mount 场景下的 useState#
直接对 useState 打断点进行调试,我们会进入 mountState 内
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
···
// initialState 为 useState 传入的初始化值
return mountState(initialState);
···
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// hooks 链表生成步骤,见[之前的文章](https://github.com/IWSR/react-code-debug/issues/2)
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// initialState 的类型声明表示它也可以是个函数
initialState = initialState();
}
// hook 上的 memoizedState、baseState 会缓存 useState 的初始值
hook.memoizedState = hook.baseState = initialState;
// 更新时会提到,比较重要的结构
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue; // useState 对应的 hooks 对象会多个 queue 属性
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any)); // 本文将分析的重点,非常重要的函数
// 对应 const [text, setText] = useState(0); 的返回值
return [hook.memoizedState, dispatch];
}
就这样 mount 阶段的 useState 的分析完了,东西很少。
另外请注意一下 basicStateReducer 这一函数。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
总结#
mount 阶段的 useState 大致起到一个初始化的作用,在结束该函数的调用后我们会获得该 useState 对应的 hooks 对象。该对象长这样
触发 useState 的 dispatch#
此时将断点打到 useState 返回的 dispatch 上,触发点击事件。获得下面的调用栈
dispatchSetState 是个关键的函数,再次强调一下。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/**
* 这里暂时理解为获取当前 update 的优先级即可
*/
const lane = requestUpdateLane(fiber);
/**
* 与 setState 的 update 链表类似
* 这里的 update 也是一个链表结构
*/
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
/**
* 判断是否是渲染中更新
*/
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
/**
* 判断 react 当前是否空闲
* 比方说在 第一次调用 dispatch 后,fiber.lanes 就不为 NoLanes
* 因此 第二次调用 dispatch 便不会进入底下对 update 实例内的 eagerState 求值
*/
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any);
/**
* 计算期望的 state 并暂存入 eagerState
* lastRenderedReducer 内逻辑如下
* 判断当前 action 是否是一个函数
* 毕竟也存在 setText((preState) => preState + 1)这样的调用方式存在
* 如果是函数传入 currentState 并调用 action 获得计算后的值
* 如果不是,比如setText(1)这样的调用方式,就直接返回 action
*/
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true; // 根据注释 防止被 reducer 重复计算
update.eagerState = eagerState;
/**
* 判断两者的值是否相等 浅比较 ===
*/
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
/**
* 根据注释,如果值未改变,则不需要去调度 react 去重新渲染
* 底下这个函数只是去处理 update 链表的结构
*/
enqueueConcurrentHookUpdateAndEagerlyBailout(
fiber,
queue,
update,
lane,
);
return; // 因为不需要调度,因此函数在这里中断
}
}
}
/**
* 因为运行到这里的 update 是被认为必须要被重新渲染到页面上的
* enqueueConcurrentHookUpdate 与 enqueueConcurrentHookUpdateAndEagerlyBailout 不同
* 会去调用 markUpdateLaneFromFiberToRoot
* 该函数会将当前 update 对应的 lane 自发生更新的 fiber 节点开始一层一层向上递归(fiber.return)
* 标记到其父 fiber 的 childLanes 上(比方说fiber.return -> fiber.return.return),直到 root 为止
* 调用 markUpdateLaneFromFiberToRoot 的目的是为 scheduleUpdateOnFiber 的调度做准备的
*/
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
/**
* 进入调度流程(scheduleUpdateOnFiber 的分析已经更新在最下面了)
* 不过在 v18 中会被 queueMicrotask 注册为 microTask
* 在 v17 则是以对应的优先级注册进 scheduler
*/
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
/**
* 一个不稳定的函数,大致上是处理 lanes,有点难懂不解析了
*/
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
总结一下 useState 的 dispatch#
对于 useState 的 dispatch 来说,每调用一次 dispathc 都会生成一个新的 update 实例且该实例会被挂载到其 hooks 的链表,但是调用 dispatch 后生成的新值会与其原始值(currentState)经过一次浅比较来决定这一次的更新是否需要被调度更新。在 v18 的版本中,这一次的调度任务是存在于 microTask 中的,因此可认为 useState 的 dispatch 属于异步操作。下面简单写个例子验证下。
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1); console.log(text); } }>{ text }</div>
)
}
export default UseState;
点击后得到的结果
其调用栈,不难看到 microtask。
update 场景下的 useState#
接下来我们稍微在原来的调试代码上添加一个 dispatch,新的调试代码大概长这样。
import { useEffect, useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => {
setText(1);
setText(2); // 给这里打个断点
} }>{ text }</div>
)
}
export default UseState;
对整个页面的表现做一个记录,大致长这样
顺带一提
第二次 dispatch 时因为会不满足 fiber.lanes === NoLanes 这个条件因此直接跳入 enqueueConcurrentHookUpdate (具体看一下 dispatchSetState 的代码)
而在 enqueueConcurrentHookUpdate 对 update 链表连接后我们会得到如下结构的环状链表
回到正题,在 useState 的 update 阶段中会调用 updateState 内的 updateReducer 这一函数(就是 uesReducer 对应的 mount 版本)
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
/**
* 这个函数在 use(Layout)Effect 内也出现过,大致作用是
* 同时移动 WIP hooks 和 current hooks 的指针向后移动一位
* 详细注释可看 [React Hooks: 附录](https://github.com/IWSR/react-code-debug/issues/4)
*/
const hook = updateWorkInProgressHook(); // WIP hooks对象
const queue = hook.queue; // update 链表
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// 在 useState 的场景中 lastRenderedReducer 为 basicStateReducer —— 在 mountState 中预设的 reducer
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
// 获取 pending 状态的 update,还未被计算
const pendingQueue = queue.pending;
/**
* 整理 update queue,为后续的更新做准备
*/
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
// 将 pendingQueue 与 baseQueue 相连
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
/**
* 处理后的 baseQueue 包含了所有 update 实例
*/
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
/**
* 筛选出被包含在 renderLanes 内的 update
* 这步骤和位操作有关
* updateLane 如果属于 renderLanes 内,则代表着当前 update 的优先级比较紧急
* 需要被处理,反之则可以跳过,
* 类似的逻辑也存在于 class 组件的 update 链表内(这里不详细展开)
* 不过这里是 ! 操作,所以都是反的
*/
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
/**
* 进入了这个逻辑的 update 便意味着不是很紧急,可以慢慢处理
* 但是这并不意味着不处理,在空闲时仍然会从被跳过的 update 开始重新再计算一次状态
* 因此需要缓存被跳过的 update 实例
*/
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
/**
* 将跳过的 update 添加到 newBaseQueue 中
* 等到下一次 render 再重新计算
*/
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
// 更新 WIP fiber 的 lanes,下一次 render 时重新执行跳过 update 的关键
// completeWork 时会将这些 lanes 收集到 root(合并到 root 发生在 commitRootImpl),然后再重新调度(commitRootImpl 内的 ensureRootIsScheduled)
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// 标记跳过的 lanes 到 workInProgressRootSkippedLanes 上
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
// 优先级足够的场景,便执行更新
if (newBaseQueueLast !== null) {
/**
* newBaseQueueLast 不为空,说明存在被跳过的 update
* update 的状态计算可能存在联系
* 因此一旦有update被跳过,就以它为起点,
* 将后边直到最后的 update 无论优先级如何都截取下来。
* (和 class 组件的处理逻辑差不多)
*/
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
// 执行本次的 update,计算新的 state
if (update.hasEagerState) {
// 已经计算过的状态会被打上标记,避免重复计算
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
// 根据 state 和 action 计算新的 state
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
// === 校验不通过,则在当前的 WIP fiber 上标记更新,在 completeWork 阶段中会被收集到 root 上
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新新的数据到 hook 内
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
/**
* 处理 interleaved update
* 但我决定跳过这一块的分析,因为我完全不知道什么样的更新才能被叫做 interleaved update
* 在 /react-reconciler/src/ReactFiberWorkLoop.old.js 内有着这一块的描述
* Received an update to a tree that's in the middle of rendering. Mark
that there was an interleaved update work on this root.
* 但是又和 enqueueConcurrentHookUpdate 与
* enqueueConcurrentHookUpdateAndEagerlyBailout 内对于 queue.interleaved 的设置完全搭不上边
* 如果有对这方面有了解的朋友也请麻烦指导一下
*/
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
总结一下#
useState 的 update 阶段会通过 dispatch 函数生成的 update 链表去更新对应的状态,然而并非所有的 update 都会被更新,只有其优先级(lane)属于当前 renderLanes 内才会被优先计算,而跳过的 update 会被标记并在下一次 render 时被更新。
简单提供个例子来证明
import { useEffect, useState, useRef, useCallback, useTransition } from "react";
function UseState() {
const dom = useRef(null);
const [number, setNumber] = useState(0);
const [, startTransition] = useTransition();
useEffect(() => {
const timeout1 = setTimeout(() => {
startTransition(() => { // 将其优先级降级,从而被 timeout2 内的 update 插队
setNumber((preNumber) => preNumber + 1);
});
}, 500 )
const timeout2 = setTimeout(() => {
dom.current.click();
}, 505)
return () => {
clearTimeout(timeout1);
clearTimeout(timeout2);
}
}, []);
const clickHandle = useCallback(() => {
console.log('click');
setNumber(preNumber => preNumber + 2);
}, []);
return (
<div ref={dom} onClick={ clickHandle }>
{
Array.from(new Array(20000)).map((item, index) => <span key={index}>{ number }</span>)
}
</div>
)
}
export default UseState;
从运行结果来看,由于 timeout1 的 update 被 startTransition 降级,因此 timeout2 的 update 被优先更新了。
批处理#
批处理其实在 v17 时代便已经有了,不过 v18 把它的限制条件给去除了然后就成了自动批处理(恼)。具体可以看看这篇文章。
比方说我们这么一段代码在运行(当然这里的调试环境依然是 v18)
import { useEffect, useState } from "react";
function UseState() {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(2);
useEffect(() => {
setNum1(11);
setNum2(22);
}, []);
return (
<>
<div>{ num1 }</div>
<div>{ num2 }</div>
</>
)
}
根据上文中对 dispatchSetState 的分析中其实有提到,只要是有必要更新到页面上的 update 就会调用 scheduleUpdateOnFiber 来触发渲染。那么例子中连续调用了两次 dispatch ,也就是说触发了两次 scheduleUpdateOnFiber,那么问题来了 —— 页面渲染了几次呢?
就一次。
至于为什么需要去看 scheduleUpdateOnFiber 里面做了什么,scheduleUpdateOnFiber 涉及到了整个更新的入口,因此也是一个比较重要的函数。
该函数主要负责处理
- 检查是否存在无限更新 —— checkForNestedUpdates
- 在 root.pendingLanes 上标记存在需要更新的 update 的 lanes —— markRootUpdated
- 触发 ensureRootIsScheduled,进入任务调度的核心函数
scheduleUpdateOnFiber#
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
// 检查是否存在无限更新,主要通过设置了一个上限 —— NESTED_UPDATE_LIMIT(50)
checkForNestedUpdates();
// Mark that the root has a pending update.
// 在 root.pendingLanes 上标记存在需要更新的 update 的 lanes
markRootUpdated(root, lane, eventTime);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
/**
* 错误处理,跳过
*/
} else {
...
/**
* 重要函数!触发任务调度的核心
* 我们要找的逻辑也在里面
*/
ensureRootIsScheduled(root, eventTime);
...
// 底下是 Legacy 模式的兼容代码,不看了
}
}
这时我们又遇到了一个 react 内的重要函数 —— ensureRootIsScheduled,进入该函数就能看到任务调度在 react 内的核心逻辑了(还有一部分核心逻辑在 Scheduler 内,可以看我写的 React Scheduler: Scheduler 源码分析)
ensureRootIsScheduled#
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
/**
* root.callbackNode 是 Scheduler 在执行调度时生成的 task,
* 而这个值会在 react 调用 scheduleCallback 时给 return 出来并赋值给 root.callbackNode
* 而从 existingCallbackNode 的变量名上不难推测,这个变量用以表示已经存在的被调度的任务(旧任务)
*/
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
/**
* 这里是为了检查正在等待的任务中是否存在已经过期了的任务(比方说一些高优先级的任务一直插队,
* 导致低优先级的任务没法被执行而过期了),如果存在过期了的任务,则把他们的 lane标记到
* expiredLanes 上,方便 react 以同步优先级立刻调用他们
*/
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
// 获取 renderLanes
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
/**
* 如果 renderLanes 为空,那么意味当前不需要启动调度,跳出
*/
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// We use the highest priority lane to represent the priority of the callback.
/**
* 生成 renderLanes 内最优先的任务对应的 优先级
*/
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
/**
* 获取旧任务的优先级
*/
const existingCallbackPriority = root.callbackPriority;
/**
* 如果两个任务的优先级相同,那么直接跳出,因为已经存在了对应的任务可以复用
* 这里也是实现 批处理 的地方
*/
if (
existingCallbackPriority === newCallbackPriority
) {
// The priority hasn't changed. We can reuse the existing task. Exit.
// 优先级未改变,因此可以复用旧任务,于是退出
return;
}
/**
* 插队逻辑
* 进入这里的逻辑都可以认为其优先级比旧任务的优先级要高,于是需要重新调度
* 于是旧任务就没有被调用的必要了,于是可以取消了。(取消逻辑可以看 Scheduler 内的逻辑)
*/
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
// 调度一个新任务
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
/**
* 同步优先级意味着这可能是过期任务或是当前的模式为非 concurrent 模式
*/
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// Flush the queue in a microtask.
/**
* 要是浏览器支持 microtask,就把 performSyncWorkOnRoot 丢到 microtask 内
* 这样子就可以更快的执行
* 一个 eventLoop 遵循 一个old MacroTask ——> 清空microtasks ——> 浏览器渲染页面 这样的顺序执行
* 不然进入 Scheduler 内,MessageChannel 处理的任务可都是 MacroTask,那就会在下一个
* eventLoop 内再渲染了
*/
scheduleMicrotask(() => {
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
/**
* 这里会把上面调度的任务取消掉然后再调用 performSyncWorkOnRoot
*/
flushSyncCallbacks();
}
});
} else {
// Flush the queue in an Immediate task.
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
// concurrent模式的处理逻辑
let schedulerPriorityLevel;
// 把 renderLanes 转成 调度优先级
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
/**
* 用对应的优先级让 Scheduler 调度 react的任务了
*/
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新root上的任务优先级和任务,以便下次发起调度时候可以获取到
// 就开头用到的 oldTask
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
分析完 ensureRootIsScheduled 我们也对批处理是如何实现的有了大致的了解,它的实现其实就是对 react 内同优先级任务的复用,这样子就做到了多次触发 hooks 的 dispatch 但是只渲染了一次。
不过到这配合上 React Scheduler: Scheduler 源码分析 其实我们已经把 react 内三大模块(调度 → 协调 → 渲染),调度的部分全部介绍完了,接下来让我一个问题来把整篇文章串起来。
React 从触发 useState 的 dispatch 到渲染到页面做了哪些事 —— 调度篇
- 生成一个对应优先级的 update 实例,并以循环链表的形式挂载到 useState 对应的 hooks 实例上的 queue 属性上
- 如果当前 update 被判断为需要被渲染到页面上的,则会将这个 update 上的 lane 自当前 fiber 节点向其父节点的 childLanes 上层层标记,直到 root 为止
- 随后会调用 scheduleUpdateOnFiber 进入调度阶段
- 首先将当前 update 的 lanes 标记到 root.pendingLanes 上,pendingLanes 上的任务可以视为所有待更新的任务
- 随后取 pendingLanes 内最高的优先级作为当前的 renderLanes
- 在使用 Scheduler 注册调度任务前,会先检查是否存在已经注册但还未执行的任务(root.callbackNode),如果存在任务便用该任务的任务优先级与 renderLanes 比较
- 任务优先级相同,则复用该任务;
- 如果 renderLanes 更高则将旧任务(root.callbackNode)取消重新在 Scheduler 内注册任务,并将返回的 task 赋值给 root.callbackNode