0%

react18.2和react16批处理实现和对比

react16 - 半自动批处理

结论

  • 执行顺序

    • 合成事件和钩子函数中:异步
    • 原生事件和setTimeout中:同步
  • 批量处理

    • 合成事件和钩子函数中的this.setState或者setState:会批量处理
    • 合成事件和钩子函数中的this.setState或者setState里面写成函数:会批量处理
    • 原生事件和setTimeout中任何情况:不会批量处理

源码分析

1. setState

1
2
3
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

2. this.updater

this.updater 是在哪个地方进行赋值暂时不用关心,只需要知道他被赋值为 classComponentUpdater

classComponentUpdaterrender 流程里面的 ReactDOM.renderscheduleRootUpdate 非常的相似,其实他们就是同一个更新原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const classComponentUpdater = {
// ......
enqueueSetState(inst, payload, callback) {
// inst 就是我们调用this.setState的this,也就是classComponent实例
// 获取到当前实例上的fiber
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
// 计算当前fiber的到期时间(优先级)
const expirationTime = computeExpirationForFiber(currentTime, fiber);

// 创建更新一个更新update
const update = createUpdate(expirationTime);

//payload是setState传进来的要更新的对象
update.payload = payload;

//callback就是setState({},()=>{})的回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}

// 把更新放到队列UpdateQueue
enqueueUpdate(fiber, update);

// 开始进入React异步渲染的核心:React Scheduler
scheduleWork(fiber, expirationTime);
},
// ......
}

状态更新都会创建一个保存更新状态相关内容的对象 Update。在 render 阶段的 beginWork 中会根据 Update 计算新的 state

这里讲这个 Update 通过 enqueueUpdate 放到队列 UpdateQueue

3. requestWork

scheduleWork 里会执行 requestWork 方法。

requestWork 中可以看到有多个 if 判断,这里就是 setState 在不同的场景使用会出现同步和异步的根本原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function requestWork(root, expirationTime) {
// 将根节点添加到调度任务中
addRootToSchedule(root, expirationTime)

// isRendering是全局变量,在后面生命周期函数我们会具体分析到
if (isRendering) {
return;
}

// isBatchingUpdates、isUnbatchingUpdates是全局变量
// react事件时有对他们进行重新赋值
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
....
performWorkOnRoot(root, Sync, false);
}
return;
}

if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}

场景1 - 合成事件

React有着一套自己的合成事件机制,在一个事件调用的时候会经过一些处理,这里不详细描述,最重要的一个函数就是 interactiveUpdates$1,在执行一个事件的时候会先调用这个函数。

1
2
3
4
5
6
7
8
9
10
handleClick(){
this.setState({
name: '吴彦祖'
})
console.log(this.state.name) // 狗蛋
this.setState({
age: '18'
})
console.log(this.state.age) // 40
}

interactiveUpdates$1方法

  • isBatchingUpdates = true;让setState不马上更新
  • try finally 语句;先执行一个事件里的代码最后才更新
  • isBatchingUpdates = previousIsBatchingUpdates;合成事件里setTimeout能马上更新的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function interactiveUpdates$1(fn, a, b) {
if (isBatchingInteractiveUpdates) {
return fn(a, b);
}

if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
// Synchronously flush pending interactive updates.
performWork(lowestPendingInteractiveExpirationTime, false, null);
lowestPendingInteractiveExpirationTime = NoWork;
}
var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true; // 把requestWork中的isBatchingUpdates标识改为true
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

isBatchingUpdates变量

interactiveUpdates$1 这个方法中把 isBatchingUpdates 设为了 true, 导致在 requestWork 方法中, isBatchingUpdatestrue ,但是 isUnbatchingUpdatesfalse ,而被直接 return 了。

1
2
3
4
5
6
7
8
//requestWork
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
// ....
performWorkOnRoot(root, Sync, false);
}
return;
}

这就导致了 requestWork 根本没有执行到任何更新的函数,比如 performSyncWork,但在最开始的 enqueueSetState 这个方法里还是已经将每一次更新都存到了一个 update 队列里。

所以合成事件里的 setState 不会马上更新,而是存入了一个更新队列里(enqueueUpdate)

try finally

interactiveUpdates$1 最后执行了一个 try finally 语法,会先执行 try 代码块中的语句,然后再执行 finally 中的代码,而 fn(a, b) 是在 try 代码块中执行相关的事件回调,而在 finally 里才有 performSyncWork();

也就是说我们写的事件监听函数在 try 中执行,但更新在 finally 里,这就导致了所谓的”异步”,state 并没有马上更新并渲染到UI上,而是等到事件执行完之后才更新的。

1
2
3
4
5
6
7
8
9
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}

场景2 - setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class App extends Component {

state = { val: 0 }

componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}

render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

try 代码块执行到 setTimeout 的时候,这是一个宏任务,把它丢到列队里,并没有去执行,而是先执行的 finally 代码块。

finally 执行的时候会执行 isBatchingUpdates = previousIsBatchingUpdates; 将 isBatchingUpdates 重置为了 false

导致最后下次事件循环的时候去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支, 可以同步拿到最新的 state 值。

场景3 - 生命周期函数中的setState

三个全局变量:isRendering、isWorking、isCommitting

  • isRendering:开始react更新就为true
  • isWorking:进入reconciler阶段就为true、进入commit阶段就为true
  • isCommitting:进入commit阶段就为true

render前生命周期属于reconciler阶段:isRendering = true、isWorking = true Fiber Reconciler 的执行阶段:

  • 阶段一是生成 Fiber 树的渐进阶段,可以被打断。
  • 阶段二是批量更新节点的阶段,不可被打断。

alt text

现在回过头来看 requestWork 里的第一个if判断:

1
2
3
4
5
6
7
function requestWork(){
// ...
if (isRendering) {
return
}
// ...
}

和合成事件一样,当 componentDidmount 执行的时候,isRenderingtrue,react内部并没有更新就先 return 了,执行完 componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmountsetState 完去 console.log 拿的结果还是更新前的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class App extends Component {
state = { val: 0 }

componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

场景4 - 原生事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends Component {

state = { val: 0 }

changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}

componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}

render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}

原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发 click 事件,到 requestWork ,在 requestWork 里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被 return,所以当你在原生事件中 setState后,能同步拿到更新后的 state 值。

场景5 - setState批量更新的情况

简单分析源码

React 加入 fiber 架构后,调度之前通过 enqueueUpdate 函数维护的 UpdateQueue 就是挂载在组件对应的 fiber 节点上,我们更新的通过调度最后会进入到 updateClassComponent 方法,里面最终会调用一个getStateFromUpdate 来获取最终的 state 状态。

getStateFromUpdate 函数外面是对 UpdateQueue 队列的一个 while 循环,比如我们连续 setState 三次,那每次都会创建一个 update 实例通过 enqueueUpdate 放入 fiberUpdateQueue 中,这里就是把这三次的state 合并计算出一个最终的值以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while (update !== null) {
// ...

/**
* resultState作为参数prevState传入getStateFromUpdate,然后getStateFromUpdate会合并生成
* 新的状态再次赋值给resultState。完成整个循环遍历,resultState即为最终要更新的state。
*/
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
// ...

// 遍历下一个update对象
update = update.next;
}

getStateFromUpdate 函数主要功能是将存储在更新对象 update 上的 partialState 与上一次的 prevState 进行对象合并,生成一个全新的状态 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function getStateFromUpdate<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {

// ...

// 调用setState会创建update对象,其属性tag当时被标记为UpdateState
case UpdateState: {
// payload 存放的是要更新的状态state
const payload = update.payload;
let partialState;

// 获取要更新的状态
if (typeof payload === 'function') {
partialState = payload.call(instance, prevState, nextProps);
} else {
partialState = payload;
}

// partialState 为null 或者 undefined,则视为未操作,返回上次状态
if (partialState === null || partialState === undefined) {
return prevState;
}

// 注意:此处通过Object.assign生成一个全新的状态state, state的引用地址发生了变化。
return Object.assign({}, prevState, partialState);
}

// ...
}

return prevState;
}
setState传入对象会合并对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class IncrementByObject extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.increment = this.increment.bind(this);
}
increment() {
// 会批量更新,只会render一次,结果是 1
this.setState({
count: this.state.count + 1
});

this.setState({
count: this.state.count + 1
});

this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div>
<button onClick={this.increment}>IncrementByObject</button>
<span>{this.state.count}</span>
</div>
);
}
}

如果是一个 Object ,直接看最后的 Object.assign({}, prevState, partialState);

Object.assign 的作用: 主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

之前提过在合成事件中或者在生命周期了 state 是不会马上刷新的,是在事件执行完后也就是 try finallyfinally 里才真正刷新,这就导致了每次 Object.assignpartialState 都是 this.state.count + 1,而 statecount 在三次 setState 的时候都不会改变都是0,所以计算过程可以简化如下:

1
2
3
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:1});

很明显最终的 statecount 只会增加 1。

setState传入函数
1
2
3
4
5
6
7
8
9
10
11
12
increment() {
// 采用传入函数的方式来更新 state,会批量,只会render一次,更新但结果是 3
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
}

如果是一个回调函数function 可以发现 if (typeof payload === 'function') 这里对传入的是否是方法做了判断,如果是方法,就执行

1
partialState = payload.call(instance, prevState, nextProps);

instance 对于类组件来说,这里保存类组件的实例在外层的 updateClassInstance函数中 const instance = workInProgress.stateNode; 赋值的。

这里其实只看 payloadprevState 就行了,payload 是我们通过 setState 传入的回调函数,返回最新的 statewhile 循环调用 getStateFromUpdate 每次传入的是 resultState,也就是说接受的 state 都是上一轮计算之后的新值,因此循环计算的过程可以简化如下:

1
2
3
Object.assign({}, {count:0}, {count:1});
Object.assign({}, {count:0}, {count:2});
Object.assign({}, {count:0}, {count:3});

可以看到最终的 statecount 为增加 3。

react18 - 自动批处理

结论

  • 执行顺序

    • 合成事件和钩子函数中:异步
    • 原生事件和setTimeout中:异步
  • 批量处理

    • 合成事件和钩子函数中:会批量处理
    • setState里面写成函数:会批量处理
    • 原生事件和setTimeout中:会批量处理

场景

具体解析可以看 《react18.2优先级和批处理场景解析》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export default function App() {
const [count, setCount] = useState(0)

console.log('render')

const onClick = () => {
// !情况1 页面变成1,render 1次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1) // 1
// !情况2 页面变成3,render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1) // 3
// !情况3 页面变成1,render 1次
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况4 页面变成1,render 2次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况5 页面变成2,render 1次
// setCount((prev) => {
// console.log('hook update 1')
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2')
// return prev + 1
// }) // 2
// !情况6 页面变成1,render 2次
// setCount((prev) => {
// console.log('hook update 1') // hook update 1
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2') // hook update 2
// return prev + 1
// }) // 2
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况7 页面变成2,render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// setCount(count + 2)
// !情况8 页面变成2,render 2次
// setCount(count + 1)
// Promise.resolve().then(() => {
// setCount(count + 2)
// })
// !情况9 页面变成2,render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// sleep(1000)
// setCount(count + 2)
}
return (
<>
<h1 onClick={onClick}>{count}</h1>
</>
)
}

概念

批处理:React 会尝试将同一上下文中触发的更新合并为一个更新。

就 React18 而言当说到批处理的时候,需同时具备以下三者:

  • 包括了多个react更新
  • 每个更新具有相同的优先级
  • 每个更新都是待执行

这样做的好处也显而易见:

  • 合并不必要的更新,减少更新流程调用次数
  • 状态按顺序保存下来,更新时不会出现「竞争问题」
  • 最终触发的更新是异步流程,减少浏览器掉帧可能性

1. 更新

对于 hook 有更新队列,对于 react 也有相应的更新(通常伴随着组件render),当然对浏览器还存在页面视图的更新。

当我们调用dispatch或者setState时,上述三种更新都是有涉及的。但是要特别指出的是,批处理中的更新就是指 react 的更新,包含了render,commit阶段等。在后续的批处理部分你将看到三者的差异。

如果我们看 dispatchSetState 的源码,会发现它们主要做了两件事:

  • 记录一次 hook 更新(enqueueConcurrentHookUpdate
  • 调度一次 react 更新(scheduleUpdateOnFiber

批中的更新就是指调度一次 react 更新 scheduleUpdateOnFiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 以下是 dispatchReducerAction 中同样包含的逻辑
// 这个函数中 fiber 和 queue 都是通过 dispatchSetState.bind 提前绑定好的,我们调用 setState 时传入的参数是 action
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// ...
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...异常情形
} else {
// ...
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 首次渲染后root !== null
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
// ...
}
}
// ...
}

2. 优先级

在更新部分的相关源码示例中,可以看到 lane 字段,它表示的就是这次更新的优先级。只有优先级相同的多个更新才在一个批中,与之相应的就是这些更新被批处理,反之则不然。

一般而言,如果优先级没有被手动改变,那么相同场景下多次调用 setState 或者 dispatch 对应的更新优先级是相同的。

例外的情况是具有一整个序列而非单一的优先级,像 TransitionLanesRetryLanes。以 TransitionLanes 为例,它们包含了许多个优先级并不相同并且依次排列的 lane,但是在 render 场景下,这些 lane 是一起被处理的。

像下面这样的示例中的更新是不会被视为同一批的,startTransition 改变了第二个更新的优先级:

1
2
setCount(count + 1)
startTransition(() => setCount(count + 2)) // startTransition引自react

3. 待执行

待执行指的是已经调度但还未被执行。通常执行相对于调度而言是异步的。假如两个更新具有相同的优先级,那么:

  • 只要一个已执行,另一个未执行,无法批处理
  • 只要都未执行,就能批处理(一些异步场景可能带来迷惑性)

对于第一点,当我们手动调用同步执行更新的api时,后续的更新就无法与同步的更新成批,在下面的示例中,你会发现点击将带来两次render。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function App() {
const [count, setCount] = useState(0)

console.log('render!')
function plus() {
flushSync(() => {
setCount(count + 1)
})
setCount(count + 2)

// 然而,这样做是可以批处理的
// flushSync(() => {
// setCount(count + 1)
// setCount(count + 2)
// })
}

return (
<div style={{ textAlign: 'center', fontSize: '42px', marginTop: '100px' }}>
<p>点击数字</p>
<span onClick={plus} onMouseEnter={plus}>
{count}
</span>
</div>
)
}

flushSync 可以使更新同步地被执行,这样一来,第二个 setCount 带来的更新与第一个 setCount 的更新无法被批处理,因为 setCount(count + 2) 调用时,第一个更新已经执行完了。

对于第二点,考虑到js事件循环带来的复杂异步特性,在一些让人意想不到的场景也能批处理,下面是一个有趣的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function App() {
const [count, setCount] = useState(0)

console.log('render!')
function plus() {
setCount(count + 1)
Promise.resolve().then(() => {
setCount(count + 2)
})
}

return (
<div style={{ textAlign: 'center', fontSize: '42px', marginTop: '100px' }}>
<p>划入render一次,点击render两次</p>
<span onClick={plus} onMouseEnter={plus}>
{count}
</span>
</div>
)
}

click事件对应的更新优先级是被调度在微任务中的,而mouseEnter事件则是另一类。

源码分析

1. setState

setState 所做的就是:

  1. hook 更新加入更新队列
  2. 尝试调度一次 react 更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// ...
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...
} else {
// ...
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 首次渲染后root !== null
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
// ...
}
}
// ...
}

2. scheduleUpdateOnFiber

忽略一些琐碎的细节后,可以发现这个函数的核心逻辑甚至更简单:

  1. 标记一次具有某一优先级的更新(markRootUpdated)
  2. 调用ensureRootIsScheduled
1
2
3
4
5
6
7
8
9
10
11
12
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ...
markRootUpdated(root, lane, eventTime);
// ...
ensureRootIsScheduled(root, eventTime);
// ...
}

3. ensureRootIsScheduled

1
2
3
4
5
6
7
8
9
10
export function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
// 这里是多次调用不再调度微任务触发processRootScheduleInMicrotask
if (!didScheduleMicrotask) {
// ? sy
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
// ...
}

4. processRootScheduleInMicrotask

1
2
3
4
5
6
7
8
9
while (root !== null) {
// ...
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
// ...
}

// 在microtask结束时,flush任何pending的同步work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。
// onClick count
flushSyncWorkOnAllRoots();

5. scheduleTaskForRootDuringMicrotask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 获取当前所有优先级中最高的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

// ...

if (includesSyncLane(nextLanes)) {
// 同步工作始终在微任务结束时刷新,因此我们不需要安排额外的任务。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// 本次要调度的优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 已经存在的调度的优先级
const existingCallbackPriority = root.callbackPriority;

if (existingCallbackPriority === newCallbackPriority) {
// 这里就是同等优先级做批处理
// ...
return;
}
// ... 高优先级打断低优先级

// 调度更新流程
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));

// ...实际的调度,最后会给root.callbackPriority赋值
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

}

关于批处理有三点:

  1. 多次调用 setState 时候,在 ensureRootIsScheduled 中通过 didScheduleMicrotask 标记,第一次进入标记为 true,再次进入便不再调度微任务触发 processRootScheduleInMicrotask
  2. 当触发后续微任务触发 processRootScheduleInMicrotask 方法,通过 getNextLanesgetHighestPriorityLane 拿到本次应该(不一定是setState时的那个)更新的优先级 newCallbackPriority
  3. 如果是同步优先级,直接return,因为processRootScheduleInMicrotask 方法最后会调用 flushSyncWorkOnAllRoots 执行一遍同步任务, 否则对比上次等待的更新和本次更新的优先级,即 existingCallbackPriority === newCallbackPriority,如果相等,则提前return,否则通过 scheduleCallback 调度更新流程