workLoop 和 performUnitOfWork
# 目录
本节的代码主要在 react-reconciler 包中。
# workLoopSync 和 workLoopConcurrent
在调和器章节中,我们注意到 performConcurrentWorkOnRoot 函数调用了 workLoopConcurrent 方法。performSyncWorkOnRoot 中调用了 workLoopSync 方法。下面我们重点看下 workLoopConcurrent 和 workLoopSync 方法的原理。代码如下:
// The fiber we're working on
let workInProgress: Fiber | null = null;
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
分析如下:
- 从代码总体上来看,不管是同步的调度还是异步的调度,到了 workLoop 这里,并不区分同步还是异步,都是调用 performUnitOfWork 方法处理。
- workInProgress 表示当前正在处理的 Fiber,本质上 performUnitOfWork 是对 workInProgress Fiber 的更新。loop 的含义就在于,只要 workInProgress 不为空,就要一直调用 performUnitOfWork 处理当前的 Fiber。
- 对于异步回调过来的任务,在 performUnitOfWork 处理每一个 Fiber 之前都要判断 shouldYield 的值是否是 false。shouldYield 是从调度器中发来的信息,表示当前是否有更高优先级的任务需要处理。对于同步任务则不需要判断,因为同步任务本身就是最高优先级的任务。如果调度器发出了新的回调,workLoopConcurrent 会被终止,新的回调 performConcurrentWorkOnRoot 中 prepareFreshStack 函数中更新 workInProgress,使 workInProgress 指向优先级更高的 FiberRoot,此后更高优先级的 workLoopConcurrent 就会触发。
- @inline 是一个告诉编译器不要 inlining 函数的标记。由于 workLoopSync 是一个会被频繁调用的函数,加此标记其实在编译器调优。更详细请参考如下文档:To Inline or Not to Inline? Enhanced Inlining Decisions (opens new window)、An annotation to either prevent or force inlining of a function (opens new window)
Inlining heavy functions results in little performance improvement. First, very few runtime function calls are eliminated. Second, the path from the caller to a heavy function is not a hot path at all, and thus will not benefit from postinlining optimization. Third, inlining heavy functions might prevent frequent edges from being inlined if the code growth budget is spent.
# performUnitOfWork
首先我们来解释一下 unitOfWork 的含义,从上文我们已经了解到不管是同步任务还是异步任务都会发起一个 workLoop,在这个 workLoop 中不要不断的调用 performUnitOfWork 来处理当前 Fiber。unitOfWork 从字面含义上来看是任务单元的意思,因此可以 performUnitOfWork 看做是执行任务单元的活动。一个完整的 workLoop 正是通过这样的任务单元积累起来的。从整个调和器来看 performUnitOfWork 才是真正干苦力的工人。
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
// 从 Fiber 版本池中获取 alternate Fiber,current 中包含 Fiber 的状态
// current 指的是已经渲染在页面上的 Fiber(current fiber),unitOfWork 指的是即将被渲染的 Fiber(pending fiber)。
const current = unitOfWork.alternate;
// 开启活动计时,只在 enableUserTimingAPI 特性开启时
startWorkTimer(unitOfWork);
setCurrentDebugFiberInDEV(unitOfWork);
let next;
// 判断 unitOfWork.mode === ProfileMode,开启了enableProfilerTimer模式,如果此时是 ProfileMode
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
// 开始执行活动,并返回下一活动
next = beginWork(current, unitOfWork, renderExpirationTime);
}
resetCurrentDebugFiberInDEV();
//
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 如果没有更多活动,则完成 workLoop
next = completeUnitOfWork(unitOfWork);
}
// 标记当前 workInProgress 的 owner component 为 null
ReactCurrentOwner.current = null;
return next;
}
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
其中比较关键的是如下代码:
next = beginWork(current, unitOfWork, renderExpirationTime);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 如果没有更多活动,则完成 workLoop
next = completeUnitOfWork(unitOfWork);
}
// 标记当前 workInProgress 的 owner component 为 null
ReactCurrentOwner.current = null;
2
3
4
5
6
7
8
9
10
从整体上来看,performUnitOfWork 处理了如下的工作:
- 调用 beginWork,将 Fiber 更新(或者创建)为 ReactElement 并返回下一个任务(child 或者是 null)。
- 将 memoizedProps 更新为 pendingProps,便于下一次 beginWork 使用。
- 如果 beginWork 之后没有任务了,则调用 completeUnitOfWork 做一些首位工作。
- 清空 ReactCurrentOwner 标记。
# beginWork
beginWork 函数比较复杂,他的大致思路如下:
function () {
// update 阶段
if(isUpdate) {
// pending fiber 优先级小于渲染的优先级
if(updateExpirationTime < renderExpirationTime) {
// 不需要执行任务,根据 pending fiber 的类型将 fiber 入 context stack;
switch (workInProgress.tag) {
case SomeType:
pushContext(workInProgress);
// 一些其他的处理;
return null || child || child.sibling 等;
}
}
}
// 根据 pending fiber 的类型,创建或者更新,返回 ReactElement
switch (workInProgress.tag) {
case SomeType:
return mountOrUpdateComponent();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
下面我们展开来看:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
// 获取 pending fiber 的过期时间
const updateExpirationTime = workInProgress.expirationTime;
// current 区分首次渲染,current !== null 表示 update 阶段
// 如果是 update 阶段可以复用 current,因此可以更新并提前返回,否则 mount 阶段就需要 mountXXXComponent
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 判断 newFiber 和 oldFiber 中 props 是否变化
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else if (updateExpirationTime < renderExpirationTime) {
// pending fiber 的优先级小于 render 优先级,不需要执行任务
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
// 根据 pending fiber 的类型推入 context stack
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
if (
workInProgress.mode & ConcurrentMode &&
renderExpirationTime !== Never &&
shouldDeprioritizeSubtree(workInProgress.type, newProps)
) {
if (enableSchedulerTracing) {
markSpawnedWork(Never);
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
return null;
}
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
if (state !== null) {
if (enableSuspenseServerRenderer) {
if (state.dehydrated !== null) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
break;
}
}
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildExpirationTime =
primaryChildFragment.childExpirationTime;
if (
primaryChildExpirationTime !== NoWork &&
primaryChildExpirationTime >= renderExpirationTime
) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// The primary children do not have pending work with sufficient
// priority. Bailout.
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
return null;
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore =
(current.effectTag & DidCapture) !== NoEffect;
const hasChildWork =
workInProgress.childExpirationTime >= renderExpirationTime;
if (didSuspendBefore) {
if (hasChildWork) {
// If something was in fallback state last time, and we have all the
// same children then we're still in progressive loading state.
// Something might get unblocked by state updates or retries in the
// tree which will affect the tail. So we need to use the normal
// path to compute the correct tail.
return updateSuspenseListComponent(
current,
workInProgress,
renderExpirationTime,
);
}
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
workInProgress.effectTag |= DidCapture;
}
// If nothing suspended before and we're rendering the same children,
// then the tail doesn't matter. Anything new that suspends will work
// in the "together" mode, so we can continue from the state we had.
let renderState = workInProgress.memoizedState;
if (renderState !== null) {
// Reset to the "together" mode in case we've started a different
// update in the past but didn't complete it.
renderState.rendering = null;
renderState.tail = null;
}
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
if (hasChildWork) {
break;
} else {
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
return null;
}
}
}
// 如果子树需要渲染,返回 child,否则返回 null。
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
//
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
// mount 阶段或者少数更新情况会执行下面的内容,进入 begin 的阶段
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
// 根据 pending fiber 的类型,返回不同的 ReactElement
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderExpirationTime,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime,
);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderExpirationTime,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime,
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime,
);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentName(type),
getCurrentFiberStackInDev,
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(
current,
workInProgress,
renderExpirationTime,
);
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
return updateFundamentalComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
简要分析下这段代码:
- beginWork 内部是区分 mount 阶段和 update 阶段来处理的,在 update 阶段只有 workInProgress 的优先级小于 render 优先级即 renderExpirationTime 时才会提前退出,返回 null 或者 child 或者其他特殊情况。这也就是说 下面的一系列的 mountComponent 或者 updateComponent 其实是在 mount 阶段和 update 阶段都会执行的。
- 两段 switch case 的代码比较复杂,之后单独分析。大致上第一段 switch case 代码段执行了一系列入栈操作在 completeWork 函数中执行了一些列的出栈操作。具体作用后面具体剖析。第二段 switch case 代码比较重要,下面来先分析分析。
# mountIndeterminateComponent
我们先来弄清楚 IndeterminateComponent 是什么?
在 shared/ReactWorkTags.js 文件中可以查到 IndeterminateComponent 的定义。从注释来看,IndeterminateComponent 应该是在未知是 FC (Function Component) 还是 CC (Class Function) 之前,为组件设定的临时类型。因此这种类型很有可能只在 mount 阶段出现,在 update 阶段会更新为 FunctionComponent 或者 ClassComponent。
另外,从 mountLazyComponent 中调用 resolveLazyComponentTag 函数来看,如果 lazy component 不被识别为 ClassComponent、FunctionComponent、ForwardRef 或者 MemoComponent,就会被认为是 IndeterminateComponent 类型。
export const IndeterminateComponent = 2; // Before we know whether it is function or class