0%

react18.2批处理场景原理并结合优先级进行的分析

事件优先级

1
2
3
4
5
6
7
// packages\react-reconciler\src\ReactEventPriorities.js

// 优先级从上往下,越来越小
export const DiscreteEventPriority: EventPriority = SyncLane; // 2
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 8
export const DefaultEventPriority: EventPriority = DefaultLane; // 页面初次渲染的lane 32, transition
export const IdleEventPriority: EventPriority = IdleLane;
1
2
3
4
5
6
7
// packages\react-reconciler\src\ReactFiberLane.js

export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; // 0
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010; // 2
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000; // 8
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000; // 32
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;

各种批处理操作场景和结果

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
export default function App() {
const [count, setCount] = useState(0)
const [page, setPage] = useState(0)

console.log('render')

const onPerform = () => {
// !setCount嵌套场景1 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// document.getElementById('onTest').click() // renderWithHooks时候执行这里,fiber和currentlyRenderingFiber相等,所以isRenderPhaseUpdate为true,这里最终走的renderWithHooksAgain
// return prev + 10
// })
// setCount((prev) => prev + 100)
// !setCount嵌套场景2 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// setCount(1000)
// return prev + 10
// })
// setCount((prev) => prev + 100)
// !setCount嵌套场景3 点击:页面变成1000 + render 2次,划入:页面变成1000 + render连续 2次
// setTimeout(() => {
// setCount((prev) => prev + 1)
// setCount((prev) => {
// document.getElementById('onTest').click()
// return prev + 10
// })
// setCount((prev) => prev + 100)
// })

// !插入一个click场景 点击:页面变成1100 + render 1次,划入:页面变成1100 + render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => {
// return prev + 10
// })
// document.getElementById('onTest').click() // 这里相当于在点击阶段就已经加入到pending中了,而且执行到这里的时候,isRenderPhaseUpdate为false,因为点击的时候没有还没有currentlyRenderingFiber
// setCount((prev) => prev + 100)

// !情况1 点击:页面变成1 + render 1次,划入:页面变成1 + render 1次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1) // 1
// !情况2 点击:页面变成3 + render 1次,划入:页面变成3 + render 1次
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1)
// setCount((prev) => prev + 1) // 3
// !情况3 两个不同的state 点击:页面count变成1,page变成-1 + render 1次,划入:页面变成count变成1,page变成-1 + render 1次
// setCount(count + 1)
// setPage(page - 1)
// !情况4 点击:页面变成1 + render 1次,划入:页面变成1 + render 1次
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况5 点击:页面变成1 + render 2次,划入:页面变成1 + render 2次
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况6 点击:页面变成2 + render 1次,划入:页面变成2 + render 1次
// setCount((prev) => {
// console.log('hook update 1')
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 2')
// return prev + 1
// }) // 2
// !情况7 点击:页面变成1 + render 2次,划入:页面变成1 + render 1次
// 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
// })
// setTimeout(() => {
// setCount(count + 1)
// setCount(count + 1)
// })
// !情况8 点击:页面变成4 + render 2次,划入:页面变成4 + render 1次
// 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
// })
// setTimeout(() => {
// setCount((prev) => {
// console.log('hook update 3') // hook update 3
// return prev + 1
// })
// setCount((prev) => {
// console.log('hook update 4') // hook update 4
// return prev + 1
// })
// })
// !情况9 点击:页面变成2 + render 2次,划入:页面变成2 + render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// setCount(count + 2)
// !情况10 点击:页面变成2 + render 2次,划入:页面变成2 + render 2次
// flushSync(() => {
// setCount(count + 1)
// })
// sleep(1000)
// setCount(count + 2)
// !情况11 点击:页面变成2 + render 2次,划入:页面变成2 + render 1次
// setCount(count + 1)
// Promise.resolve().then(() => {
// setCount(count + 2)
// })
}
// !情况12 页面变成1 + render 1次
// const changeValue = () => {
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// // setCount((prevState, props) => ({
// // count: prevState.count + 1,
// // }));
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// console.log(count) // react18 输出的是更新前的值 --> 0
// }
// document.body.addEventListener('click', changeValue, false)

function sleep(milliseconds) {
const now = Date.now()
while (Date.now() - now < milliseconds) {}
}

return (
<>
<h1 onClick={onPerform} onMouseEnter={onPerform}>
Hello CodeSandbox {count}
</h1>
</>
)
}

执行流程和原理

dispatchSetState 的源码在 setState文章中 展示过一次。这里只是重复什么是批-更新的说法,通过点击事件或者鼠标划入事件触发setState,其实就是调用 dispatchSetState 方法。dispatchSetState 所做的就是:

  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
30
31
32
33
34
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {

const lane = requestUpdateLane(fiber);
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
// ...
} else {

// ...

// ! 2. 把update暂存到concurrentQueues数组中
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// ! 3. 调度更新
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}

// ...
}

如果不纠结 fiberqueue 的细节的话,就批处理而言,这就是 setState的全部了。

步骤1:获取本次更新的优先级 - requestUpdateLane

dispatchSetState 方法在 创建update 前,会通过 requestUpdateLane 方法取到 update 的优先级。

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
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
// 1. 非ConcurrentMode模式 2. 目前不支持
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}

// 普通更新与非紧急更新(18)
const transition = requestCurrentTransition();
// 如果有transition
if (transition !== null) {
const actionScopeLane = peekEntangledActionLane();
return actionScopeLane !== NoLane
? // We're inside an async action scope. Reuse the same lane.
actionScopeLane
: // We may or may not be inside an async action scope. If we are, this
// is the first update in that scope. Either way, we need to get a
// fresh transition lane.
requestTransitionLane(transition);
}

// React内部的一些update,比如flushSync,会通过上下文变量来跟踪其优先级
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
// ? sy setState click 2
return updateLane;
}

// React外部的update,根据事件类型,向当前环境获取对应的优先级。
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}

getCurrentEventPriority

1
2
3
4
5
6
7
8
9
export function getCurrentEventPriority(): EventPriority {
const currentEvent = window.event;
if (currentEvent === undefined) {
// ? sy 页面初次渲染
return DefaultEventPriority;
}

return getEventPriority(currentEvent.type);
}

getEventPriority

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
export function getEventPriority(domEventName: DOMEventName): EventPriority {
switch (domEventName) {
// Used by SimpleEventPlugin:
case 'cancel':
case 'click':
case 'close':
case 'contextmenu':
case 'copy':
case 'cut':
case 'auxclick':
case 'dblclick':
case 'dragend':
case 'dragstart':
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
case 'paste':
case 'pause':
case 'play':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'touchcancel':
case 'touchend':
case 'touchstart':
case 'volumechange':
// Used by polyfills: (fall through)
case 'change':
case 'selectionchange':
case 'textInput':
case 'compositionstart':
case 'compositionend':
case 'compositionupdate':
// Only enableCreateEventHandleAPI: (fall through)
case 'beforeblur':
case 'afterblur':
// Not used by React but could be by user code: (fall through)
case 'beforeinput':
case 'blur':
case 'fullscreenchange':
case 'focus':
case 'hashchange':
case 'popstate':
case 'select':
case 'selectstart':
return DiscreteEventPriority;
case 'drag':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'scroll':
case 'toggle':
case 'touchmove':
case 'wheel':
// Not used by React but could be by user code: (fall through)
case 'mouseenter':
case 'mouseleave':
case 'pointerenter':
case 'pointerleave':
return ContinuousEventPriority;
case 'message': {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}

可以看到,通过一系列操作:

  • click 事件对应的优先级是 DiscreteEventPriority,优先级值也就是 2
  • mouseenter 对应的优先级是 ContinuousEventPriority,优先级值也就是 8
  • setTimeout 对应的优先级的值是默认优先级 32

一般在合成事件创建之初,就会通过 createEventListenerWrapperWithPriority 各个事件的设置优先级,所以一般 clickmouseenter 等react内部的事件,都会通过 getCurrentUpdatePriority 拿到 updateLane
类似 setTimeout,会走到 getCurrentEventPriority 返回 DefaultEventPriority
如果是通过 document.body.addEventListener('click', changeValue, false) 触发的事件,也会走到 getCurrentEventPriority,通过getEventPriority 拿到 window.event 所对应的优先级

总结

requestUpdateLane 中获取优先级:

  1. 先通过requestCurrentTransition请求并判断是否有非紧急更新
  2. 再通过getCurrentUpdatePriority
  3. 最后通过getCurrentEventPriority根据事件类型,项当前环境获取对应的优先级。

步骤2:创建 update 对象

1
2
3
4
5
6
7
8
9
10
11
// ...
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// ...

步骤3:scheduleUpdateOnFiber

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

  • 标记一次具有某一优先级的更新(markRootUpdated)
  • 调用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);
// ...
}

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);
}
// ...
}

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();

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;

}

执行流程小结

通过微任务调度 processRootScheduleInMicrotask 方法,此方法核心就是调用 scheduleTaskForRootDuringMicrotask,而在 microtask 结束时,调用 flushSyncWorkOnAllRoots 方法 flush 任何 pending 的同步 work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。

scheduleTaskForRootDuringMicrotask 中:

  1. 先是检查是否有饿死的任务,如果是,将它们标记为过期,这样我们就知道下一个要处理的是它们
  2. 获取当前所有优先级中最高的优先级 nextLanes
  3. 先判断是否包含同步优先级,如果包含,取消其它任务并返回,不包含就继续执行
  4. 再判断现存的任务优先级和新任务的优先级,如果相等,就合并执行且 return
  5. 如果不相等,就取消现存任务
  6. 继续根据 nextLanes,通过 lanesToEventPriority 方法,获取 schedulerPriorityLevel 调度的优先级,最后通过 scheduleCallback 进行调度

调用 scheduleTaskForRootDuringMicrotask 中的两大问题

请看《react18.2高优先级任务插队和饥饿任务问题分析》

updateReducerImpl

我们以函数式组件的处理 setState 时的实际执行函数 updateReducerImpl 为例,来看一下相关处理:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {

const queue = hook.queue;

if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}

// 记录reducer
queue.lastRenderedReducer = reducer;

// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue; // ! 单向循环链表

// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 如果有 pendingQueue,说明有未处理的更新
// 把pendingQueue添加到baseQueue中
if (baseQueue !== null) {
// pending queue合并到base queue中。
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
// baseQueue 被设置为 pendingQueue,也就是待更新的 update 对象队列
current.baseQueue = baseQueue = pendingQueue;
// 需要清空 pending 队列,这样本次执行 hook 的 pending 队列不会影响到下一次
queue.pending = null;
}

const baseState = hook.baseState;
// 如果基础更新队列中有 update 对象,说明需要执行更新,就按照优先级依次处理每一个 update 对象(下面会进行批量更新)
if (baseQueue === null) {
// 如果没有pendingQueue,那么memoizedState应该等于baseState
hook.memoizedState = baseState;
} else {
// 有待处理的queue,接下来遍历即可
const first = baseQueue.next;
let newState = baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null; // 跳过的这些更新(低优先级任务)会被保存在这个循环链表中
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
// An extra OffscreenLane bit is added to updates that were made to
// a hidden tree, so that we can distinguish them from updates that were
// already there when the tree was hidden.
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;

// Check if this update was made while the tree was hidden. If so, then
// it's not a "base" update and we should disregard the extra base lanes
// that were added to renderLanes when we entered the Offscreen tree.
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);

// 当前任务的优先级不够,也就是说当前的 renderLanes 比该 update 对象的优先级高。需要先跳过,之后再处理。
if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// 如果在当前被跳过的 update 对象之前没有其他的 update 被跳过,该对象就是作为新的基础更新对象。并把最初跳过任务时候的 baseState 存储起来
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
// 如果之前有更新被跳过,那么将这个更新对象添加到队列最后
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 更新当前正在工作的 Fiber 节点(workInProgress)的优先级,标记这个更新对象的优先级由于不匹配当前的 renderLane,因此已经被跳过。
// 在同文件的 renderWithHook() 方法中可以知道 currentlyRenderingFiber 对应了 workInProgress,表示当前正在工作的 fiber 树
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else { // 优先级足够
// 优先级足够的时候理论上可以考虑对哪些 update 对象进行收集更新,但是此时还需要考虑一个 revertLane(还原的优先级),以保证组件状态的正确。
// This update does have sufficient priority.

// Check if this is an optimistic update.
const revertLane = update.revertLane;
if (!enableAsyncActions || revertLane === NoLane) {
// ? sy
// This is not an optimistic update, and we're going to apply it now.
// But, if there were earlier updates that were skipped, we need to
// leave this update in the queue so it can be rebased later.
// 如果 newBaseQueueLast 不为 null,证明有跳过的更新,要把当前的update也加入newBaseQueueLast只是到其 lane 会赋值为 0,后面进行低优先级的更新时,这些已经被处理过的更新也仍旧会被处理,保证最后计算的数据是正确的
if (newBaseQueueLast !== null) {
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,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}

// Check if this update is part of a pending async action. If so,
// we'll need to suspend until the action has finished, so that it's
// batched together with future updates in the same action.
if (updateLane === peekEntangledActionLane()) {
// ? sy-no
didReadFromEntangledAsyncAction = true;
}
} else {
// This is an optimistic update. If the "revert" priority is
// sufficient, don't apply the update. Otherwise, apply the update,
// but leave it in the queue so it can be either reverted or
// rebased in a subsequent render.
if (isSubsetOfLanes(renderLanes, revertLane)) {
// The transition that this optimistic update is associated with
// has finished. Pretend the update doesn't exist by skipping
// over it.
update = update.next;

// Check if this update is part of a pending async action. If so,
// we'll need to suspend until the action has finished, so that it's
// batched together with future updates in the same action.
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
const clone: Update<S, A> = {
// Once we commit an optimistic update, we shouldn't uncommit it
// until the transition it is associated with has finished
// (represented by revertLane). Using NoLane here works because 0
// is a subset of all bitmasks, so this will never be skipped by
// the check above.
lane: NoLane,
// Reuse the same revertLane so we know when the transition
// has finished.
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
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.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}

// 该 update 对象的优先级足够,因此开始处理它的 action,收集新的 state 状态
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action);
}
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 {
// ! 计算useReducer的新的state
newState = reducer(newState, action);
}
}
// 循环 baseQueue(实际上就是 pendingQueue),处理该队列中的每个 update 对象,
// 并把对应的 action 操作得到的结果更新到 newState 中(即收集新状态)
update = update.next;
} while (update !== null && update !== first);

if (newBaseQueueLast === null) {
newBaseState = newState; // 如果没有跳过的任务,那就把最终计算后的结果存到newBaseState,最终赋值给baseState,否则baseState就还是第一个被跳过的任务时可以拿到的baseState,也是保证最后执行低优先级任务计算的数据是正确的
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();

if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
// 更新到hook上
hook.memoizedState = newState;

hook.baseState = newBaseState;
// 如果一直没有跳过的,newBaseQueueLast就为null,如果有跳过的就不为null也就赋值给了baseQueue,之后低优先级任务重启后可以执行baseQueue上的任务,当执行最后一批低优先级任务时newBaseQueueLast为null,baseQueue也就为null了,不会影响以后再次触发任务
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}

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];
}

从上面的源码可以看到,updateReducerImpl 方法核心内容分为四个部分:

  • 获取 Hook 对象中的更新队列、基础更新队列、基础状态、reduce 函数等信息。
  • 如果更新队列中有待处理的更新对象,就将其加入到基础更新队列中。
  • 按照优先级高低区分是否跳过该更新或者依次处理基础更新队列中的更新对象,计算新的状态。
  • 判断新的状态和旧的状态是否相同,如果相同就不做重新渲染,反之需要重新渲染。
  • 返回更新后的状态和 dispatch 方法。

针对setState嵌套场景的分析

当执行嵌套的 setState 时候,会执行 dispatchSetState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {

const lane = requestUpdateLane(fiber);
// ! 1. 创建update
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// setState嵌套setState的情况,是同一个fiber
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
// ...
}
}
1
2
3
4
5
6
7
8
// 判断是否是同一个fiber,如果发生嵌套的情况,是一个fiber,会返回true
function isRenderPhaseUpdate(fiber: Fiber): boolean {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}

update 加入到队列中,并将 didScheduleRenderPhaseUpdateDuringThisPass 置为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
): void {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}

当有 setState 嵌套发生,是发生在 render 阶段的 renderWithHooks 中。执行完函数组件 Component() 会进行判断, 当didScheduleRenderPhaseUpdateDuringThisPasstrue 会执行 renderWithHooksAgain

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
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// ...

let children = Component(props, secondArg);

// Check if there was a render phase update
// enqueueRenderPhaseUpdate会给didScheduleRenderPhaseUpdateDuringThisPass设置为true,所以下面还会render一次,这也就是setState嵌套setState的情况下render连续 2次
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering until the component stabilizes (there are no more render
// phase updates).
children = renderWithHooksAgain(
workInProgress,
Component,
props,
secondArg,
);
}

// ...
}

总结

所以通过对执行流程和原理的分析,对于以上提到的这么多批处理的场景,也就能通过源码并结合eventLoop的知识解释的通了。

关于批处理的逻辑,主要有三点:

  • 通过 getNextLanesgetHighestPriorityLane 拿到本次应该(不一定是setState时的那个)更新的优先级 newCallbackPriority
  • 对比上次等待的更新和本次更新的优先级,即 newCallbackPriority === existingCallbackPriority,如果相等,则提前 return
  • 在更新阶段,以 setState 为例,会根据优先级来判断是否要同一批次执行

scheduleUpdateOnFiber 中已经对 setState 对应的优先级做了标记,所以那个优先级在这里是可以被读取到的。如果两次更新的优先级相同,批处理就会起作用。

批处理的发生当然意味着代码进入上述的 newCallbackPriority === existingCallbackPriority 分支内。但是即使是其他情形也有可能进入这个分支。
例如,当连续的两次 setState 被调用,前者优先级高于后者,那么当第二次 setState 被调用,从而进入 ensureRootIsScheduled 时,existingCallbackPriority与newCallbackPriority 都是第一次调用时的优先级(每次所取的都是最高优先级),导致函数提前返回。这并不意味着低优先级的更新被忽略,在高优先级的更新即将完成时,ensureRootIsScheduled 会被再次调用,确保所有更新会被执行。