0%

1. 加载阶段

1.1 updateClassComponent

packages/react-reconciler/src/ReactFiberBeginWork.js

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
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {

let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
// 类组件节点的stateNode 不是存储dom元素,而是组件实例 【hostComponent的stateNode才是dom元素】
const instance = workInProgress.stateNode;
let shouldUpdate;
# 第一次class组件加载,instance都为null
if (instance === null) {
// 初始化构建组件实例
constructClassInstance(workInProgress, Component, nextProps);
// 加载组件实例
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// update阶段:判断是否更新class组件
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}

# 最后:创建class组件child
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
# 返回child子节点
return nextUnitOfWork;
}

updateClassComponent 方法的内容并不复杂,主要就是两个逻辑的执行:

  1. class组件实例的创建或者更新。
  2. 创建组件的child子节点,最后返回子节点。

1.2 构建class组件实例 - constructClassInstance

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 constructClassInstance(
workInProgress: Fiber,
ctor: any,
props: any,
): any {

# 创建class实例对象,参数为props和context
let instance = new ctor(props, context);

// 将instance实例对象的state数据同步到Fiber节点的memoizedState属性
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);

# 确定class组件实例:即链接FiberNode与对应的组件实例
adoptClassInstance(workInProgress, instance);

// dev开发环境下的警告:如果class组件使用了过时的生命周期钩子,发出对应的警告
...

# 返回创建完成的实例
return instance;
}

1.2.1 adoptClassInstance

1
2
3
4
5
6
7
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
// Fiber节点存储instance实例
workInProgress.stateNode = instance;
// instance对象定义一个_reactInternal内部属性存储Fiber节点
setInstance(instance, workInProgress);
}

1.3 加载组件 - mountClassInstance

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
function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): void {

// 取出class组件实例
const instance = workInProgress.stateNode;
// props
instance.props = newProps;
// 同步state数据
instance.state = workInProgress.memoizedState;
// ref:默认为空对象
instance.refs = emptyRefsObject;
// 初始化一个Fiber节点的更新队列
# 设置更新队列对象:fiber.updateQueue = queue;
initializeUpdateQueue(workInProgress);

// 同步组件实例的state数据
instance.state = workInProgress.memoizedState;

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
// 调用getDerivedStateFromProps钩子
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
instance.state = workInProgress.memoizedState;
}

// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
// 不应该使用旧的生命周期钩子
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
# 触发WillMount生命周期钩子
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}

# 如果设置了class组件的componentDidMount生命周期钩子函数,则需要在组件的FiberNode上设置对应的flags
if (typeof instance.componentDidMount === 'function') {
let fiberFlags: Flags = Update;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStatic;
}

workInProgress.flags |= fiberFlags;
}
}

首先更新了 instance 组件实例上的一些属性,然后初始化了当前组件 Fiber 节点的 updateQueue 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function initializeUpdateQueue<State>(fiber: Fiber): void {
// 创建一个更新队列对象
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState, // 初始state数据
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null, // 存储update链表
lanes: NoLanes,
},
effects: null,
};

// 设置updateQueue属性
fiber.updateQueue = queue;
}

然后就是针对 componentWillMount 生命周期钩子函数的处理,虽然是过时的API,但是如果定义了还是需要在这里触发。

最后如果定义了 componentDidMount 钩子函数,则需要给该 Fiber 节点的 flags 属性设置对应的副作用标记。

flags 标记的作用是在 commit 阶段执行对应的副作用操作,所以 componentDidMount 钩子函数会在 commit 阶段中进行触发调用。

到此,类组件的加载就完成了。

2. 更新阶段

1
2
3
4
5
# setState原理
Component.prototype.setState = function(partialState, callback) {
// 调用updater中的一个方法
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

2.1 enqueueSetState

packages/react-reconciler/src/ReactFiberClassComponent.js

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
const classComponentUpdater = {
isMounted,
enqueueSetState(inst: any, payload: any, callback) {
// ! 1. 获取current和lane
const fiber = getInstance(inst);
const lane = requestUpdateLane(fiber);

// ! 2. 创建update
const update = createUpdate(lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// ! 3. update入队fiber.updateQueue中, 并返回应用的root根节点
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
// ! 4. 调度更新(开启一个从root根节点开始的更新调度)
scheduleUpdateOnFiber(root, fiber, lane);
// ! 5. 处理transitions,非紧急更新
entangleTransitions(root, fiber, lane);
}

if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
},
enqueueReplaceState(inst: any, payload: any, callback: null) {
// ...
},
enqueueForceUpdate(inst: any, callback) {
// ...
},
};

2.1.1 enqueueUpdate

1
2
3
4
5
6
7
8
9
10
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
): FiberRoot | null {
const updateQueue = fiber.updateQueue;
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}

2.1.2 enqueueConcurrentClassUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
function enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) {
const interleaved = sharedQueue.interleaved;
// 第一个update对象入队
if (interleaved === null) {
update.next = update;
pushConcurrentUpdateQueue(sharedQueue);
} else {
// 其他的update对象入队
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
}

回到 enqueueSetState 方法中,这个方法最后会调用 scheduleUpdateOnFiber 函数进入更新的调度程序。

2.2 performSyncWorkOnRoot

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
export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}

const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// If passive effects were flushed, exit to the outer work loop in the root
// scheduler, so we can recompute the priority.
// TODO: We don't actually need this `ensureRootIsScheduled` call because
// this path is only reachable if the root is already part of the schedule.
// I'm including it only for consistency with the other exit points from
// this function. Can address in a subsequent refactor.
ensureRootIsScheduled(root);
return null;
}

if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}

// * setState
// !1. render阶段
let exitStatus = renderRootSync(root, lanes);
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}

if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes, NoLane);
ensureRootIsScheduled(root);
throw fatalError;
}

if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes, workInProgressDeferredLane);
ensureRootIsScheduled(root);
return null;
}

// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// ! 2. commit阶段
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
workInProgressDeferredLane,
);

// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root);

return null;
}

2.3 updateClassInstance

packages/react-reconciler/src/ReactFiberClassComponent.js

1
2
3
4
5
6
7
8
9
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): boolean {
// ...
}

updateClassInstance 方法里面的内容比较多,我们分成以下几个部分来分析

2.3.1 根据updateQueue计算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
// 取出组件实例
const instance = workInProgress.stateNode;
# 从旧节点上克隆updateQueue信息,包含了shared.pending中等待处理的更新操作
cloneUpdateQueue(current, workInProgress);
// 旧的props
const unresolvedOldProps = workInProgress.memoizedProps;
const oldProps =
workInProgress.type === workInProgress.elementType
? unresolvedOldProps
: resolveDefaultProps(workInProgress.type, unresolvedOldProps);
instance.props = oldProps;
// 新的props
const unresolvedNewProps = workInProgress.pendingProps;
...

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
typeof getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function';

// 取出旧的数据
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
# 更新组件实例的数据
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;

首先处理之前的 updateQueue 信息:

这里就是将 current 节点中的 updateQueue 信息克隆到 workInProgress 节点中对应的属性中。
注意:react应用每次更新都会执行Fiber Reconciler流程【即FiberTree的创建流程】,在进入此流程之前,current节点会存储本次更新相关的一些信息,但是在进入此流程之后,current就变成了旧的节点,workInProgress代表新建的节点,此时就需要将current节点上的一些信息保存到新的节点之中,也就是对应的workInProgress。

从class类中取出 getDerivedStateFromProps 钩子,判断当前类组件有没有使用此钩子,如果没有使用则设置变量 hasNewLifecyclesfalse,此变量的作用是后续判断其他生命周期钩子的执行与否。

下面开始对state的处理:

  1. 首先取出旧的state数据:
  2. 重点:调用processUpdateQueue方法,根据updateQueue信息,计算生成新的state数据。
2.3.1.1 processUpdateQueue

packages/react-reconciler/src/ReactFiberClassUpdateQueue.js

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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes,
): void {

didReadFromEntangledAsyncAction = false;

// This is always non-null on a ClassComponent or HostRoot
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);

hasForceUpdate = false;

let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;

// Check if there are pending updates. If so, transfer them to the base queue.
// *1. 检查是否有pending update。如果有,需要将它们剪开环型链表,合并到 baseQueue。
// pending update是个单向循环链表,转移到 单链表 firstBaseUpdate->...->lastBaseUpdate 中去
// ! 这里注意pending update不同于baseQueue,pending update只记录了尾节点
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// ? sy
queue.shared.pending = null;

// The pending queue is circular. Disconnect the pointer between first
// and last so that it's non-circular.
// ! 这里的 pendingQueue 是在 finishQueueingConcurrentUpdates 函数中构建的单向循环链表
const lastPendingUpdate = pendingQueue; // 尾节点
const firstPendingUpdate = lastPendingUpdate.next; // 头结点
lastPendingUpdate.next = null; // 断开循环链表
// Append pending updates to base queue
// 把pending update转移到base queue上
// ! 接下来构建单链表 firstBaseUpdate->...->lastBaseUpdate
if (lastBaseUpdate === null) {
// base queue是空的,那么firstPendingUpdate就是头节点firstBaseUpdate
firstBaseUpdate = firstPendingUpdate;
} else {
// 否则往这个尾节点后继续加
lastBaseUpdate.next = firstPendingUpdate;
}
// 更新尾节点, 将 lastBaseUpdate 赋值为 lastPendingUpdate
// 此时已经形成了 以 firstBaseUpdate 为头以 lastBaseUpdate 为尾的新链表
// 也即为本次需要处理的 update 链表
lastBaseUpdate = lastPendingUpdate;

// If there's a current queue, and it's different from the base queue, then
// we need to transfer the updates to that queue, too. Because the base
// queue is a singly-linked list with no cycles, we can append to both
// lists and take advantage of structural sharing.
// TODO: Pass `current` as argument
const current = workInProgress.alternate;
// 如果有current queue,并且它和base queue不同,那么我们也需要把更新转移到那个queue上。
if (current !== null) {
// This is always non-null on a ClassComponent or HostRoot
// 类组件和 HostRoot 的updateQueue都初始化过,所以这里不会是null
const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
// 如果current的lastBaseUpdate和baseQueue的lastBaseUpdate不同,那么把pending update转移到currentQueue上
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}

// These values may change as we process the queue.
if (firstBaseUpdate !== null) {
// *2. 接下来要做的就是遍历queue,然后根据这些update,计算出最后的结果。
// Iterate through the list of updates to compute the result.
let newState = queue.baseState;
// TODO: Don't need to accumulate this. Instead, we can remove renderLanes
// from the original lanes.
let newLanes = NoLanes;
// 这里的 newBaseState, newFirstBaseUpdate,newLastBaseUpdate 是计算的临时变量
// 实际上会用来更新 updateQueue 的 baseState, firstBaseUpdate, lastBaseUpdate
let newBaseState = null;
let newFirstBaseUpdate = null;
let newLastBaseUpdate: null | Update<State> = null;

let update: Update<State> = firstBaseUpdate;
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);

// 更新优先级不满足,该 update 会被跳过
if (shouldSkipUpdate) {
// ? sy-no
// 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<State> = {
lane: updateLane,

tag: update.tag,
payload: update.payload,
callback: update.callback,

next: null,
};
if (newLastBaseUpdate === null) {
newFirstBaseUpdate = newLastBaseUpdate = clone;
newBaseState = newState;
} else {
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// Update the remaining priority in the queue.
newLanes = mergeLanes(newLanes, updateLane);
} else {
// 该 update 更新优先级满足,本次更新不会跳过
// This update does have sufficient priority.

// 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 !== NoLane && updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}

// 如果 newLastBaseUpdate 不存在,说明之前没有跳过任何 upadte 无需添加新增
// 否则无论无论该 update 是否跳过都需要添加到 baseUpdate 链表之后
if (newLastBaseUpdate !== null) {
// ? sy-no
const clone: Update<State> = {
// 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,

tag: update.tag,
payload: update.payload,

// When this update is rebased, we should not fire its
// callback again.
callback: null,

next: null,
};
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}

// Process this update.
// 处理这个更新
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);
// 类组件的setState会在这里存储
const callback = update.callback;
if (callback !== null) {
workInProgress.flags |= Callback;
if (isHiddenUpdate) {
workInProgress.flags |= Visibility;
}
const callbacks = queue.callbacks;
if (callbacks === null) {
queue.callbacks = [callback];
} else {
callbacks.push(callback);
}
}
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
// 下一个update
update = update.next;
if (update === null) {
// 已经到达尾节点,所有update被处理完毕,暂停循环
pendingQueue = queue.shared.pending;
// 这里与函数组件有所不同,当setState套setState时候,虽然这种情况都会加入到.pending中
// 但是class组件会在最后判断,如有就继续执行
// 而函数组件添加时dispatchSetState会走enqueueRenderPhaseUpdate添加任务,不会走scheduleUpdateOnFiber这个分支,在renderWithHooks中Component()执行结束后会走renderWithHooksAgain,再次执行一遍组件
// this.setState((prevState, props) => {
// this.setState({ count: 100 })
// return {
// count: prevState.count + 1,
// }
// })
if (pendingQueue === null) {
break;
} else {
// An update was scheduled from inside a reducer. Add the new
// pending updates to the end of the list and keep processing.
// 已经计划在 reducer 内部进行更新。将新的待处理更新添加到列表末尾并继续处理。
const lastPendingUpdate = pendingQueue;
// Intentionally unsound. Pending updates form a circular list, but we
// unravel them when transferring them to the base queue.
const firstPendingUpdate =
((lastPendingUpdate.next: any): Update<State>);
lastPendingUpdate.next = null;
update = firstPendingUpdate;
queue.lastBaseUpdate = lastPendingUpdate;
queue.shared.pending = null;
}
}
} while (true);

if (newLastBaseUpdate === null) {
newBaseState = newState;
}

// 更新 updateQueue 的 baseState,firstBaseUpdate, lastBaseUpdate 三个属性
queue.baseState = ((newBaseState: any): State);
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;


if (firstBaseUpdate === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
// 当多个transitions在同一个queue中时,只允许最近的一个完成。我们不应该显示中间状态。
// 当queue为空,我们将queue.lanes设置回0
queue.shared.lanes = NoLanes;
}

// Set the remaining expiration time to be whatever is remaining in the queue.
// This should be fine because the only two other things that contribute to
// expiration time are props and context. We're already in the middle of the
// begin phase by the time we start processing the queue, so we've already
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
// 把跳过的update的lane记录下来
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState; // 更新状态
}
}

2.3.2 调用getDerivedStateFromProps钩子

1
2
3
4
5
6
7
8
9
10
if (typeof getDerivedStateFromProps === 'function') {
// 调用getDerivedStateFromProps钩子
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
newState = workInProgress.memoizedState;
}

如果类组件定义了 getDerivedStateFromProps 钩子函数,则在此触发此回调。

2.3.3 调用shouldComponentUpdate钩子,检查组件是否应该更新

1
2
3
4
5
const shouldUpdate =
// 检查是否为强制更新
checkHasForceUpdateAfterProcessing() ||
// 调用shouldComponentUpdate钩子,检查是否应该更新
checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext)
2.3.3.1 checkHasForceUpdateAfterProcessing

checkHasForceUpdateAfterProcessing 方法返回一个变量的状态:

1
2
3
export function checkHasForceUpdateAfterProcessing(): boolean {
return hasForceUpdate;
}

hasForceUpdate 是一个全局变量,它默认为 false ,表示非强制更新,它的修改就在之前计算state的逻辑中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
newState = getStateFromUpdate(...)


function getStateFromUpdate<State>() {
switch (update.tag) {
case ReplaceState: {}
case CaptureUpdate: {}
case UpdateState: {}
// 强制更新 场景
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
}

如果为 this.forceUpdate 触发的场景,就会进入 ForceUpdate 分支,更新变量 hasForceUpdate 的值为 true

这时在校验组件是否应该更新时,就会返回 true,代表组件需要更新。

而我们当前是通过 this.setState 修改数据触发的更新,所以当前 hasForceUpdate 是为 false 的,这也是绝大部分类组件更新的场景。

2.3.3.2 checkShouldComponentUpdate

查看 checkShouldComponentUpdate

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
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
let shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
return shouldUpdate;
}

// 针对PureComponent纯组件的 内部校验
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}

return true;
}

checkShouldComponentUpdate 方法也比较简单,主要就是调用一次我们定义的 shouldComponentUpdate 钩子,根据调用的返回值来决定类组件是否应该更新,如果我们没有使用 shouldComponentUpdate 钩子,则类组件是默认需要更新的。

同时在这里我们还可以发现有一个针对 PureComponent 纯组件的更新校验,这其实就是 PureComponentComponent 唯一的区别,纯组件 PureComponent在react内部自动帮助我们对 propsstate 进行了浅比较,任何一个变化则返回 true,需要更新组件。

2.3.4 为组件Fiber的flags属性设置componentDidUpdate和getSnapshotBeforeUpdate副作用标记

1
2
3
4
5
6
7
8
9
10
11
# 执行componentWillUpdate钩子
if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(newProps, newState, nextContext);
}
// 更新flags标记
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.flags |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
workInProgress.flags |= Snapshot;
}

如果类组件定义了 componentWillUpdate 生命周期钩子函数,则会在此处调用此函数。

然后如果类组件定义了 componentDidUpdate 或者 getSnapshotBeforeUpdate 生命周期钩子函数,就会更新组件 Fiberflags 副作用标记。

这些生命周期钩子会在commit阶段,真实DOM渲染完成之后,被触发执行。

最后同步更新组件实例instance的props和state:

1
2
instance.props = newProps;
instance.state = newState;

到此,一个类组件的更新程序基本执行完成。

2.4 commit阶段

前面全部的加载逻辑都是在Fiber Reconciler协调流程中执行的,即类组件大部分的加载或者更新逻辑都是在reconciler协调流程中完成的,还有剩下的一部分逻辑在commit阶段之中处理。

对于类组件的更新来说,在commit阶段主要还有以下两部分逻辑需要处理:

  1. 执行类组件的 componentDidUpdate 生命周期钩子函数。
  2. 执行 this.setState 方法传入的回调函数。

packages/react-reconciler/src/ReactFiberCommitWork.js

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
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型:进行不同的处理
switch (finishedWork.tag) {

// 类组件的处理
case ClassComponent: {
// 组件实例
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// mount加载阶段

# 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
instance.componentDidMount();
} else {

// update更新阶段
const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
: resolveDefaultProps( finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
# 触发componentDidUpdate 生命周期钩子函数
instance.componentDidUpdate( prevProps,prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}

# 取出当前组件节点的updateQueue更新对象
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}

...
}
}
}

finishedWork代表当前处理的类组件Fiber节点,首先从fiber.stateNode属性中取出组件实例instance。然后根据进行判断,只有存在相关的副作用标记才会继续内部的逻辑:

然后判断current是否为null,current表示旧的虚拟DOM节点,在组件的更新阶段,它肯定是存在的。然后直接调用 componentDidUpdate 生命周期钩子函数即可。

3. 总结

类组件的更新逻辑主要内容是以下两点:

  1. reconciler 协调流程中循环 update 链表计算出最新的 state
  2. commit 阶段中触发 componentDidUpdate 生命周期钩子函数以及循环执行 this.setState 的回调。

核心算法 - 最小堆

packages\scheduler\src\SchedulerMinHeap.js

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
type Heap<T: Node> = Array<T>;
type Node = {
id: number,
sortIndex: number,
...
};

export function push<T: Node>(heap: Heap<T>, node: T): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}

export function peek<T: Node>(heap: Heap<T>): T | null {
return heap.length === 0 ? null : heap[0];
}

export function pop<T: Node>(heap: Heap<T>): T | null {
if (heap.length === 0) {
return null;
}
const first = heap[0];
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
}

function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}

function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];

// If the left or right node is smaller, swap with the smaller of those.
if (compare(left, node) < 0) {
if (rightIndex < length && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}

function compare(a: Node, b: Node) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}

任务调度器

通过调用 scheduleCallback 方法进入调度器

1
2
3
4
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);

从下面开始,我们将深入scheduler的源码:

1. 定义全局变量

packages\scheduler\src\forks\Scheduler.js

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
import {push, pop, peek} from '../SchedulerMinHeap';
import {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
LowPriority,
IdlePriority,
} from '../SchedulerPriorities';

import {
enableIsInputPending,
enableIsInputPendingContinuous,
frameYieldMs,
continuousYieldMs,
maxYieldMs,
userBlockingPriorityTimeout,
lowPriorityTimeout,
normalPriorityTimeout,
} from '../SchedulerFeatureFlags';

import {
markTaskRun,
markTaskYield,
markTaskCompleted,
markTaskCanceled,
markTaskErrored,
markSchedulerSuspended,
markSchedulerUnsuspended,
markTaskStart,
stopLoggingProfilingEvents,
startLoggingProfilingEvents,
} from '../SchedulerProfiling';

export type Callback = boolean => ?Callback;

export opaque type Task = {
id: number,
callback: Callback | null,
priorityLevel: PriorityLevel,
startTime: number,
expirationTime: number,
sortIndex: number,
isQueued?: boolean,
};

// 获取当前时间,通过performance或者Date
let getCurrentTime: () => number | DOMHighResTimeStamp;
const hasPerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
const localPerformance = performance;
getCurrentTime = () => localPerformance.now();
} else {
const localDate = Date;
const initialTime = localDate.now();
getCurrentTime = () => localDate.now() - initialTime;
}

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// 最大的31位整数。V8中32位系统的最大整数。
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;

// Tasks are stored on a min heap
var taskQueue: Array<Task> = []; // 没有延迟的任务
var timerQueue: Array<Task> = []; // 有延迟的任务

// 自增id,标记task唯一性
// Incrementing id counter. Used to maintain insertion order.
var taskIdCounter = 1;

// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;

var currentTask = null;
var currentPriorityLevel = NormalPriority;

// 锁
// 是否有work在执行
// This is set while performing work, to prevent re-entrance.
var isPerformingWork = false;

// 主线程是否在调度
var isHostCallbackScheduled = false;

let isMessageLoopRunning = false;

// 是否有任务在倒计时
var isHostTimeoutScheduled = false;

let taskTimeoutID: TimeoutID = (-1: any);

let needsPaint = false;

// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom

// 判断当前是否有所有类型的输入事件,包括按键、鼠标、滚轮触控等DOM UI事件
const isInputPending =
typeof navigator !== 'undefined' &&
// $FlowFixMe[prop-missing]
navigator.scheduling !== undefined &&
// $FlowFixMe[incompatible-type]
navigator.scheduling.isInputPending !== undefined
? navigator.scheduling.isInputPending.bind(navigator.scheduling)
: null;

// 是否应该把控制权交换给主线程
function shouldYieldToHost(): boolean {
// 当前程序运行时间
const timeElapsed = getCurrentTime() - startTime;
// 如果运行时间小于帧间隔时间5ms
if (timeElapsed < frameInterval) {
return false;
}

// The main thread has been blocked for a non-negligible amount of time. We
// may want to yield control of the main thread, so the browser can perform
// high priority tasks. The main ones are painting and user input. If there's
// a pending paint or a pending input, then we should yield. But if there's
// neither, then we can yield less often while remaining responsive. We'll
// eventually yield regardless, since there could be a pending paint that
// wasn't accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
if (enableIsInputPending) {
if (needsPaint) {
// There's a pending paint (signaled by `requestPaint`). Yield now.
return true;
}
if (timeElapsed < continuousInputInterval) {
// We haven't blocked the thread for that long. Only yield if there's a
// pending discrete input (e.g. click). It's OK if there's pending
// continuous input (e.g. mouseover).
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
// Yield if there's either a pending discrete or continuous input.
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
// We've blocked the thread for a long time. Even if there's no pending
// input, there may be some other scheduled work that we don't know about,
// like a network event. Yield now.
return true;
}
}

// `isInputPending` isn't available. Yield now.
return true;
}

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
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
// There's a few reasons for why we prefer setImmediate.
//
// Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
// (Even though this is a DOM fork of the Scheduler, you could get here
// with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
// https://github.com/facebook/react/issues/20756
//
// But also, it runs earlier which is the semantic we want.
// If other browsers ever implement it, it's better to use it.
// Although both of these would be inferior to native scheduling.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}

3. 任务调度器的入口函数

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
// 任务调度器的入口函数:并发模式下调度一个回调函数 【这里传入的callback就是performConcurrentWorkOnRoot】
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: {delay: number},
): Task {
var currentTime = getCurrentTime(); // 获取当前程序执行时间,performance.now()

var startTime; // 定义任务开始时间,不是执行时间
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
// 如果存在延期,则开始时间 = 当前时间 + 延期时间
startTime = currentTime + delay;
} else {
// 否则,开始时间 = 当前时间
startTime = currentTime;
}
} else {
// 开始时间直接等于currentTime
startTime = currentTime;
}

// 定义超时时间 【根据优先级,设置不同的超时时间】
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately,立即超时
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out,最终超时
timeout = userBlockingPriorityTimeout; // 250
break;
case IdlePriority:
// Never times out,永不超时
timeout = maxSigned31BitInt; // Math.pow(2, 30) - 1, 最大的31位整数。V8中32位系统的最大整数。
break;
case LowPriority:
// Eventually times out,最终超时
timeout = lowPriorityTimeout; // 10000
break;
case NormalPriority:
default:
// Eventually times out,最终超时
timeout = normalPriorityTimeout; // 5000
break;
}

// 过期时间,也是理论上的任务执行时间,值越小,说明优先级越高,需要优先执行
var expirationTime = startTime + timeout;

var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1, // 调度执行任务的依据
};

// 开始时间 大于当前时间:说明是延期任务,先加入到延时队列timerQueue
if (startTime > currentTime) {
// 有delay的任务
newTask.sortIndex = startTime; // sortIndex是把任务从timerQueue中取出来放到taskQueue中的依据
push(timerQueue, newTask); // 暂时存到timerQueue,等晚点到了执行时间,再放到taskQueue,再执行任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 所有任务都延迟了,而这是延迟时间最短的任务。
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
// 取消现有的setTimeout
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
// setTimeout
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 没有delay的任务,直接加入任务队列
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
// 如果需要的话,调度一个HostCallback。如果我们已经在执行work,就等到下次我们让出控制权的时候。
// 判断host回调任务是否已经被调度,以及是否正在工作中,只有host回调任务还没有被调度 且 当前并未在工作中;才会开启一个新的host回调任务
// 【首次加载时,需要调度一个host回调任务】
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// 设置host回调任务,触发localSetImmediate、MessageChannel或localSetTimeout,生成新的宏任务,在宏任务中执行工作循环workLoop
requestHostCallback();
}
}

return newTask;
}

4. 调度异步执行,创建新的宏任务

1
2
3
4
5
6
7
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 调度异步执行,创建新的宏任务
schedulePerformWorkUntilDeadline();
}
}

5. 异步执行宏任务

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
// 在有效时间内执行工作
const performWorkUntilDeadline = () => {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
// 记录了一个work的起始时间,其实就是一个时间切片的起始时间,是个时间戳
startTime = currentTime;

// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
// remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
// 根据返回判断是否还有工作
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// 如果还有任务,则又触发调度宏任务事件,生成新的宏任务,即在下一个event loop中继续执行任务
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};

function flushWork(initialTime: number) {
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}

isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
return workLoop(initialTime);
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}

// 有很多task,每个task都有一个callback,callback执行完了,就执行下一个task
// 一个work就是一个时间切片内执行的一些task
// 时间切片要循环,就是work要循环
// 返回为true,表示还有任务没有执行完,需要继续执行
function workLoop(initialTime: number) {
let currentTime = initialTime;
// 如果timerQueue中有*有效任务*到达执行时间,就放到taskQueue中
advanceTimers(currentTime);
// 从任务队列中取出队列第一个任务【注意:taskQueue中是按任务的到期时间expirationTime排序的,越小越先执行】
currentTask = peek(taskQueue);
// 循环从taskQueue中取出任务
while (currentTask !== null) {
/**
* 【重点判断】
* 1,如果当前任务到期时间 大于 当前时间,说明任务还未过期
* 2,shouldYieldToHost为true应该暂停
* 总结:如果同时满足这两个条件,即任务还没过期,但是没有剩余可执行时间了,就应该跳出本次工作循环,
* 让出主线程,交给渲染流水线,等待下一个宏任务执行task
*/
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
break;
}
// 执行任务
const callback = currentTask.callback;
if (typeof callback === 'function') {
// 有效的任务
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// 是否属于过期的任务,可能存在还没过期的任务。
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行任务
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 如果执行完后又返回了 function,赋值给当前任务的callback
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
return true;
} else {
// 否则的话,将当前任务移除。中断在这个位置发生,高优先任务会把低优先任务的callback置空。
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
// 返回是否还有任务
return true;
} else {
// 说明 currentTask 执行完了
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
// 处理 timerQueue
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}

// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let frameInterval = frameYieldMs;
const continuousInputInterval = continuousYieldMs;
const maxInterval = maxYieldMs;
let startTime = -1;

const continuousOptions = {includeContinuous: enableIsInputPendingContinuous};

function advanceTimers(currentTime: number) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// 无效的任务
// Timer was cancelled.
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// 有效的任务
// 任务已到达开始时间,转入taskQueue中
// Timer fired. Transfer to the task queue.
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}

function handleTimeout(currentTime: number) {
isHostTimeoutScheduled = false;
// 把延迟任务从timerQueue中推入taskQueue
advanceTimers(currentTime);

if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback();
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}

function cancelCallback(task: Task) {
// Null out the callback to indicate the task has been canceled. (Can't
// remove from the queue because you can't remove arbitrary nodes from an
// array based heap, only the first one.)
task.callback = null;
}

function getCurrentPriorityLevel(): PriorityLevel {
return currentPriorityLevel;
}

function requestHostTimeout(
callback: (currentTime: number) => void,
ms: number,
) {
taskTimeoutID = localSetTimeout(() => {
callback(getCurrentTime());
}, ms);
}

function cancelHostTimeout() {
localClearTimeout(taskTimeoutID);
taskTimeoutID = ((-1: any): TimeoutID);
}

export {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
IdlePriority,
LowPriority,
scheduleCallback,
cancelCallback,
getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield,
getCurrentTime,
};

核心流程图

1. 使用了 Scheduler 任务调度的流程图(Concurrent模式)

alt text

2. 没有使用 Scheduler 任务调度的流程图(legacy模式)

alt text

总结

一个比较泛的流程示例:
alt text

  • 在 React 中宏观来看,针对浏览器、Scheduler 、Reconciler 其实是有3层 Loop。浏览器级别的 eventLoop,Scheduler 级别的 workLoop,Reconciler 级别 workLoopConcurrent 。
    • 浏览器的 eventLoop 与 Scheduler 的关系
      • 每次 eventLoop 会执行宏任务的队列的宏任务,而 React 中的 Scheduler 就是用宏任务 setImmediate等 触发的。
      • 当 eventLoop 开始执行跟 Scheduler 有关的宏任务时,Scheduler 会启动一次 workLoop,就是在遍历执行 Scheduler 中已存在的 taskQueue 队列的每个 task。
    • Scheduler 与 Reconciler 的关系
      • Scheduler中的 workLoop 中每执行一次 task,是通过调用 Reconciler 中的 performConcurrentWorkOnRoot 方法,即每一个 task 可以理解为是一个 performConcurrentWorkOnRoot 方法的调用。
      • performConcurrentWorkOnRoot 方法每次调用,其本质是在执行 workLoopConcurrent 方法,这个方法是在循环 performUnitOfWork 这个构建 Fiber 树中每个 Fiber 的方法。

因此可以梳理出来,3个大循环,从最开始的 eventLoop 的单个宏任务执行,会逐步触发 Scheduler 和 Reconciler 的任务循环执行。

  • 任务的中断与恢复,实现中断与恢复的逻辑分了2个部分,第一个是 Scheduler 中正在执行的 workLoop 的任务中断,第二个是 Reconciler 中正在执行的 workLoopConcurrent 的任务中断
    • Reconciler 中的任务中断与恢复:在 workLoopConcurrent 的 while 循环中,通过 shouldYield() 方法来判断当前构建 fiber 树的执行过程是否超时,如果超时,则中断当前的 while 循环。由于每次 while 执行的 fiber 构建方法,即 performUnitOfWork 是按照每个 fiberNode 来遍历的,也就是说每完成一次 fiberNode 的 beginWork + completeWork 树的构建过程,会设置下一次 nextNode 的值 ,可以理解为中断时已经保留了下一次要构建的 fiberNode 指针,以至于不会下一次不知道从哪里继续。
    • Scheduler 中的任务中断与恢复:当执行任务时间超时后,如果 Reconciler 中的 performConcurrentWorkOnRoot 方法没有执行完成,会返回其自身。在 Scheduler 中,发现当前任务还有下一个任务没有执行完,则不会将当前任务从 taskQueue 中取出,同时会把 reconciler 中返回的待执行的回调函数继续赋值给当前任务,于是下一次继续启动 Scheduler 的任务时,也就连接上了。同时退出这次中断的任务前,会通过 messageChannel 向 eventLoop 的宏任务队列放入一个新的宏任务。
    • 所以任务的恢复,其实就是从下一次 eventLoop 开始执行 Scheduler 相关的宏任务,而执行的宏任务也是 Reconciler 中断前赋值的 fiberNode,也就实现了整体的任务恢复。

1
2
3
4
5
// !3. commit
// 我们现在有了一个一致的树。下一步要么是 commit,要么是,如果有什么被暂停了,就等待一段时间后再 commit。
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);

2. commit阶段 - finishConcurrentRender

2.1 finishConcurrentRender

packages\react-reconciler\src\ReactFiberWorkLoop.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
) {
// ...
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
);
}

2.2 commitRootWhenReady

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function commitRootWhenReady(
root: FiberRoot,
finishedWork: Fiber,
recoverableErrors: Array<CapturedValue<mixed>> | null,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
lanes: Lanes,
spawnedLane: Lane,
) {
// ...

// Otherwise, commit immediately.
commitRoot(
root,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
);
}

2.3 commitRoot

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
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
spawnedLane: Lane,
) {
// TODO: This no longer makes any sense. We already wrap the mutation and
// layout phases. Should be able to remove.
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;

try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(
root,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
previousUpdateLanePriority,
spawnedLane,
);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}

return null;
}

2.4 commitRootImpl

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
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
) {

// ...

const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;

// ...

if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;

pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
// ! 1. 异步执行 passive effects
flushPassiveEffects();
return null;
});
}
}

// 检查HostFiber的子孙元素是存在副作用
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
// 检查HostFiber自身存在副作用
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;

// 任何一个有副作用,说明需要更新,进入commit逻辑
if (subtreeHasEffects || rootHasEffect) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
// 获取当前更新优先级
const previousPriority = getCurrentUpdatePriority();
// 设置同步优先级,commit必须同步执行
setCurrentUpdatePriority(DiscreteEventPriority);
// 当前上下文
const prevExecutionContext = executionContext;

// ! 2. 进入commit阶段
executionContext |= CommitContext;

// 在调用生命周期之前将其重置为null
ReactCurrentOwner.current = null;

// ! 3. BeforeMutation阶段【这个阶段执行与flags相关的副作用】
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);

// ...

// ! 4. mutation阶段 (完成真实的dom渲染,更新页面)
commitMutationEffects(root, finishedWork, lanes);

// 渲染后,重置container容器信息
resetAfterCommit(root.containerInfo);

// 根据Fiber双缓冲机制,完成Current Fiber Tree的切换
// 其实也并不一定叫切换,就是将最新work内容存储为当前的内容,下一次的work就可以利用当前的内容
// 注意:到这里是页面已经完成了更新渲染,这个交互Fiber Tree是为了保留最新的Tree,提供给下次更新使用
// 同时也方便调用生命周期钩子时是最新的DOM
root.current = finishedWork;

// ! 5. layout阶段
commitLayoutEffects(finishedWork, root, lanes);

// ...

} else {
// No effects.
root.current = finishedWork;
}

// ...

return null;
}

2.4.1 异步执行 passive effects

执行 useEffect 的 effects

2.4.2 进入 commit 阶段

1
executionContext |= CommitContext

2.4.3 beforeMutation阶段 - commitBeforeMutationEffects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
): boolean {
focusedInstanceHandle = prepareForCommit(root.containerInfo);

// 设置第一个处理的节点为 hostFiber
nextEffect = firstChild;
// 提交开始
commitBeforeMutationEffects_begin();

const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;

return shouldFire;
}

初始化 nextEffect 变量,这里的 firstChild 代表第一个处理的节点为 hostFiber

然后调用 commitBeforeMutationEffects_begin,开始递归遍历 FiberTree,处理有副作用的 Fiber 节点。

2.4.3.1 commitBeforeMutationEffects_begin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function commitBeforeMutationEffects_begin() {
// while循环,处理所有节点
while (nextEffect !== null) {
const fiber = nextEffect;
// 取出子节点, 刚开始是hostFiber节点的child: 即App 组件
const child = fiber.child;
// 如果该fiber的子节点存在BeforeMutation阶段相关的flgas标记 且 child不为null; 则继续循环,
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber;
// 设置child为下一个处理的节点
nextEffect = child;
} else {
// 只有当fiber的子节点不存在BeforeMutation阶段相关的flgas标记 且 child为null; 【叶子节点】
// 和reconciler流程一样,第一个进入complete工作的都是【div react源码调试】节点
commitBeforeMutationEffects_complete();
}
}
}
2.4.3.2 commitBeforeMutationEffects_complete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
// 处理当前节点的副作用
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
// 取出兄弟节点
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
// 将兄弟节点设置为下一个nextEffect 【回到begin工作】
nextEffect = sibling;
return;
}

nextEffect = fiber.return;
}
}

commitBeforeMutationEffects_complete方法也是一个while循环,对当前Fiber节点执行flags标记对应的操作,即执行commitBeforeMutationEffectsOnFiber方法。执行完成后,如果当前Fiber节点存在sibling兄弟节点,则将兄弟节点设置为最新的nextEffect,退出当前函数,开启兄弟节点的begin工作。如果不存在兄弟节点,则将当前Fiber节点的父级节点设置为最新的nextEffect,执行父级节点的commitBeforeMutationEffects_complete工作。

根据上面可以看出,BeforeMutation阶段逻辑和之前创建FiberTree的逻辑基本相同,都是深度优先遍历的顺序从HostFiber根节点开始【自上而下】遍历处理每一个满足条件的Fiber节点,执行flags对应的副作用操作,即相似的begin和complete工作逻辑。这里主要的执行内容在commitBeforeMutationEffectsOnFiber方法之中。

注意,其实commit阶段中三个子阶段的逻辑:基本都是以这种逻辑方式来处理相关的副作用内容。

2.4.3.3 commitBeforeMutationEffectsOnFiber
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
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;

// 根据节点的类型,进行不同的副作用处理
switch (finishedWork.tag) {
case FunctionComponent: {
if (enableUseEffectEventHook) {
if ((flags & Update) !== NoFlags) {
commitUseEffectEventMount(finishedWork);
}
}
break;
}
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if ((flags & Snapshot) !== NoFlags) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
break;
}
// hostFiber节点的处理
case HostRoot: {
if ((flags & Snapshot) !== NoFlags) {
if (supportsMutation) {
// 应用根节点
const root = finishedWork.stateNode;
// 设置textContent = ''; 也就是清空#div容器内容, 方便Mutation阶段的渲染
clearContainer(root.containerInfo);
}
}
break;
}
case HostComponent:
case HostHoistable:
case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break;
default: {
if ((flags & Snapshot) !== NoFlags) {
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
}

if ((flags & Snapshot) !== NoFlags) {
resetCurrentDebugFiberInDEV();
}
}

进入 commitBeforeMutationEffectsOnFiber 方法后,发现只有 Snapshot 标记的副作用才会执行。

hostFiber 节点的 Snapshot 副作用是在 completeWork 工作中被标记的。

2.4.3.4 BeforeMutation阶段总结

所以BeforeMutation阶段最终只会执行这两种副作用:

  1. 触发类组件的getSnapshotBeforeUpdate钩子。
  2. 处理HostRoot类型节点【HostFiber】,清空#root容器内容, 方便下面Mutation阶段的DOM挂载。

2.4.4 mutation 阶段 - commitMutationEffects

前面我们已经知道了 MutationMask 的定义:

1
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;

MutationMask 就是代表 Mutation 阶段所需要执行的哪些副作用类型,虽然可以看到 MutationMask 集成了很多的副作用标记,但是 Mutation 阶段的重点内容:还是针对 Fiber 节点上 DOM 内容的处理,然后将最终的 DOM 内容渲染到页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;

commitMutationEffectsOnFiber(finishedWork, root, committedLanes);

inProgressLanes = null;
inProgressRoot = null;
}
2.4.4.1 commitMutationEffectsOnFiber
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
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {

// 旧hostFiber 节点
const current = finishedWork.alternate;
// 取出dom操作的标识flags
const flags = finishedWork.flags;

// 根据节点tag,执行不同的逻辑
switch (finishedWork.tag) {

// 函数组件处理
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// recursivelyTraverse:递归遍历
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// app组件跳出了循环,向下继续执行
commitReconciliationEffects(finishedWork);

if (flags & Update) {
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}

// 类组件处理
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}

// 原生dom元素处理
case HostComponent: {
// 递归遍历
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 处理dom
commitReconciliationEffects(finishedWork);

if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
// 如果flags标记为ContentReset
if (finishedWork.flags & ContentReset) {
// dom实例
const instance: Instance = finishedWork.stateNode;
try {
// 重置dom的内容
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}

// 如果flgas标记为Update更新
if (flags & Update) {
// 取出当前节点对应的DOM对象
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}

// 文本处理
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}

const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;

try {
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}

// hostFiber根节点处理 【第一次会走根节点, 从根节点向下递归渲染dom】
case HostRoot: {
// recursivelyTraverse:递归遍历 【页面显示】
recursivelyTraverseMutationEffects(root, finishedWork, lanes);

commitReconciliationEffects(finishedWork);

if (flags & Update) {
if (supportsMutation && supportsHydration) {
if (current !== null) {
const prevRootState: RootState = current.memoizedState;
if (prevRootState.isDehydrated) {
try {
commitHydratedContainer(root.containerInfo);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
if (supportsPersistence) {
const containerInfo = root.containerInfo;
const pendingChildren = root.pendingChildren;
try {
replaceContainerChildren(containerInfo, pendingChildren);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}

// ...

default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

return;
}
}
}

commitMutationEffectsOnFiber 方法内容很多,但是它的核心逻辑依然是switch case结构:根据当前Fiber节点tag值,对不同组件类型进行不同的逻辑处理。

要处理的组件类型有很多,我们主要掌握几个常见的组件类型处理逻辑:

  • FunctionComponent:函数组件。
  • ClassComponent:类组件。
  • HostComponent:DOM节点。
  • HostText:文本节点。
  • HostRoot:HostFiber根节点。

从上面几个组件类型的处理逻辑来看,它们都是一套相同的处理逻辑:

1
2
3
4
5
// 1,删除DOM
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 2,插入DOM
commitReconciliationEffects(finishedWork);
// 3,更新(DOM内容)
2.4.4.2 Mutation阶段执行顺序
1
2
3
4
5
6
commitMutationEffectsOnFiber  => commit()
recursivelyTraverseMutationEffects 这个方法中使用了循环,并且调用了上面的方法=> for()
// 2,插入DOM
commitReconciliationEffects(finishedWork); // 2,3两部分视为一个内容 => content()
// 3,更新(DOM内容)
commitPlacement(finishedWork);

所以Mutation阶段的执行顺序就可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Fiber顺序: HostFiber  =>    fun App    => 	 div.App  =>   ...
// 代码执行顺序
commit
for
commit
for
commit
for
commit
...
content
content
content

执行顺序为从上往下,通过这种递归遍历方式,可以发现HostFiber虽然是第一个进入commit逻辑的节点,但是它的content内容却是最后一个执行,也就是说在HostFiber的content内容处理完成之后,即代表完整的DOM树已经渲染到页面。实际上最后执行插入到页面的操作是在【fun App】组件节点上执行的,后续会有说明,这部分内容在第三章也有解释。

alt text

2.4.4.3 recursivelyTraverseMutationEffects
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
function recursivelyTraverseMutationEffects(
root: FiberRoot, // root
parentFiber: Fiber, // HostFiber
lanes: Lanes,
) {
# 删除标记,是否存在
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 执行删除副作用
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}

# 如果子节点树有副作用标记
if (parentFiber.subtreeFlags & MutationMask) {
// 取出子节点
let child = parentFiber.child;
while (child !== null) {
// 开始递归渲染
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}

根据上面的代码可以看出,recursivelyTraverseMutationEffects 方法主要就两个逻辑:

  • 判断当前 Fiber 节点是否存在 deletions 删除标记,如果存在则循环 deletions,删除子节点对应DOM元素的内容。
  • 遍历子树,递归调用 commitMutationEffectsOnFiber

这里主要讲解第一点内容删除的逻辑,第二点遍历的内容就是前面 Mutation 阶段执行顺序的内容。
具体的删除逻辑就是执行 commitDeletionEffects 方法,真实的删除逻辑比较复杂,删除一个DOM元素要考虑很多东西,这里我们不会展开 commitDeletionEffects 方法,但是还是要了解一下可能会造成的一些副作用影响:

  • 执行子树所有组件的 unmount 卸载逻辑。
  • 执行子树某些类组件的 componentWillUnmount 方法。
  • 执行子树某些函数组件的 useEffectuseLayoutEffect等 hooks 的 destory 销毁方法。
  • 执行子树所有 ref 属性的卸载操作。

这里将删除DOM的逻辑放在每个组件的第一个执行,也是非常必要的。因为Mutation阶段的核心就是DOM操作,如果对应的DOM都已经被删除了,那么也就没有必要再去执行剩下的修改更新了。

2.4.4.4 commitReconciliationEffects
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
// 如果是插入/移动标记
if (flags & Placement) {
try {
# 执行dom插入添加操作
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}

commitReconciliationEffects 方法就是执行 DOM 的插入或者移动操作,判断当前 Fiber 节点是否存在 Placement 标记,存在就会执行 commitPlacement 方法,执行相关的 DOM 的操作。

2.4.4.5 commitPlacement
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
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}

// finishedWork: fun App 它的父级节点为HostFIber
const parentFiber = getHostParentFiber(finishedWork);

switch (parentFiber.tag) {
# 普通DOM节点
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}

const before = getHostSibling(finishedWork);
// 插入节点
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}

# 处理HostFiber节点的插入动作:
case HostRoot:
case HostPortal: {
// #div
const parent: Container = parentFiber.stateNode.containerInfo;
// 无兄弟节点
const before = getHostSibling(finishedWork);
# 将离屏的DOM树插入到#div; 马上页面上显示出DOM内容
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
}
}

能够执行 Placement 副作用的,只有两种组件节点 HostComponentHostRoot

对于 HostComponent 来说,就是常规的 DOM 插入和移动,即调用原生的 DOM 方法执行对应的逻辑:

1
2
3
// 原生DOM操作
parentNode.appendChild()
parentNode.insertBefore()
mutation阶段总结

mutation 阶段的主要工作是:

  • DOM 内容的增删改操作,最后将构建完成的离屏 DOM 树渲染到页面。
  • 针对函数组件触发 useInsertionEffect hook 的副作用以及 useLayoutEffect hookdestroy 方法。
  • 针对类组件和普通 DOM 组件重置 ref 对象的内容。

2.4.5 layout 阶段 - commitLayoutEffects

前面我们已经知道了 LayoutMask 的定义:

1
LayoutMask = Update | Callback | Ref | Visibility;

LayoutMask 就是代表 Layout 阶段所需要执行的哪些副作用类型:

  • 类组件的 componentDidMount/componentDidUpdate 生命周期钩子函数的执行。
  • 类组件调用 this.setState 时传递的 callback 回调函数的会被保存到 Fiber 节点的 updateQueue 属性中在这里执行。
  • 执行函数组件的 useLayoutEffect hook 回调。

可以说 Layout 阶段的主要内容:就是在DOM渲染完成后,执行函数组件和类组件定义的一些callback回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;

// HostFiber
nextEffect = finishedWork;
# 又是begin 和complete工作逻辑
# 相当于递归循环触发每个组件的生命周期钩子函数 以及相关的 hooks 回调
commitLayoutEffects_begin(finishedWork, root, committedLanes);

inProgressLanes = null;
inProgressRoot = null;
}
2.4.5.1 commitLayoutEffects_begin
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 commitLayoutEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

// 循环触发
while (nextEffect !== null) {
// HostFiber
const fiber = nextEffect;
// fun App组件节点
const firstChild = fiber.child;

# 说明子树存在副作用,需要更新nextEffect为子节点,进入下一级的循环
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
# 说明副作用在自身节点了,进入complete阶段
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
2.4.5.2 commitLayoutMountEffects_complete
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
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// while循环
while (nextEffect !== null) {
const fiber = nextEffect;
// 存在layout相关的副作用 才会进入
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
setCurrentDebugFiberInDEV(fiber);
try {
# 执行副作用
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}

# 当回到HostFiber时,代表循环完成,退出layout工作
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}

# 存在兄弟节点,开始兄弟节点的工作
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
# 不存在兄弟节点,则返回父级节点
nextEffect = fiber.return;
}
}
2.4.5.3 commitLayoutEffectOnFiber
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
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
# 根据组件类型:进行不同的处理
switch (finishedWork.tag) {
# 1,函数组件的处理
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
// 处理副作用
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
break;
}

# 2,类组件的处理
case ClassComponent: {
// 组件实例
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// mount加载阶段

# 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
instance.componentDidMount();
} else {

// update更新阶段
const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
: resolveDefaultProps( finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
# 触发componentDidUpdate 生命周期钩子函数
instance.componentDidUpdate( prevProps,prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}

# 取出当前组件节点的updateQueue更新对象
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostComponent: {
// 取出DOM对象
const instance: Instance = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
# 针对一些特殊的DOM元素做加载处理:button,input等做自动聚焦,对Img图片做加载
commitMount(instance, type, props, finishedWork);
}
break;
}

...

}
}
}

commitLayoutEffectOnFiber 方法依然是和前面两个阶段的逻辑一样,根据不同的组件节点进行不同的逻辑处理,这里我们还是理解几个重点组件类型即可。

  • 函数组件

    1
    commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

    同步执行当前函数组件节点的useLayoutEffect 回调。

  • 类组件
    根据当前Fiber节点是否存在current【旧的Fiber】来区分是mount阶段还是update阶段:

    • mount阶段:执行当前类组件的componentDidMount生命周期钩子函数。
    • update阶段:执行当前类组件的componentDidUpdate生命周期钩子函数。
layout阶段总结

在真实DOM加载完成后:

  • 执行函数组件的useLayoutEffect hook的回调。
  • 类组件执⾏ componentDidMount 或者 componentDidUpdate。。
  • 由此可⻅,函数组件的 effects 和类组件中⽣命周期执⾏时机是不同的

FiberTree的切换

1
2
3
4
5
6
// 2,Mutation阶段
commitMutationEffects(root, finishedWork, lanes);
# FiberTree的切换
root.current = finishedWork;
// 3, Layout阶段
commitLayoutEffects(finishedWork, root, lanes);

Mutation 阶段和 Layout 阶段之间还有一个重要处理没有说明,那就是 FiberTree 的切换。

通过前面我们已经知道,Mutation 阶段处理完成之后,页面就已经完成了真实的 DOM 渲染。所以此时finishedWork 就是最新的 FiberTree 以及存储着最新的 DOM 内容,在这里更新 current 的内容,主要有两个方面的作用:

  • 保留最新的 Fiber 树结构,在下一次更新时就可以利用本次的 FiberTree 来做数据的复用以及差异对比。
  • 对于类组件来说:当在 Layout 阶段执行 componentDidMount 或者 componentDidUpdate 生命周期钩子时就可以获取最新的 DOM 内容。

总结

到此为止,react18.2源码解析最重要的内容:即一个react应用的基本加载流程算是解析完成了,虽然其中一些逻辑描述可能不够准确,但整体来说还是比较符合。当然其中也有一些逻辑的细节并没有展开讲解,这是因为本身应用的加载流程就已经比较复杂了,如果在解析过程中每个逻辑都放在一起讲解,那文章的内容可能翻倍不止,阅读效果也会大打折扣。所以关于一些细节方面的逻辑处理会在新的章节里面展开讲解,比如类组件,函数组件的具体加载过程、hooks原理、合成事件等等。

react源码分析(三)render-render阶段

如果以下代码没有特殊标记,路径都是 src/react/packages/react-reconciler/src/ReactFiberWorkLoop.js

这个 performConcurrentWorkOnRoot 函数中包含两个重要的阶段,rendercommit 阶段。这是每个并发任务的入口点。

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
// This is the entry point for every concurrent task, i.e. anything that goes through Scheduler.
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}

// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
// ??
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
} else {
// Current task was not canceled. Continue.
}
}

// Determine the next lanes to work on, using the fields stored
// on the root.
// TODO: This was already computed in the caller. Pass it as an argument.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}

// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// 在某些情况下,我们会禁用时间片切片:如果work过长时间做计算(为了防止饥饿而将其视为“过期”的work),或者我们处于默认启用同步更新模式。
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) && // 参考这个例子,UseDeferredValuePage
(disableSchedulerTimeoutInWorkLoop || !didTimeout);

// ! 1. render
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes) // 参考这个例子,UseDeferredValuePage
: renderRootSync(root, lanes); // ? sy, 不用时间切片

if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes, NoLane);
} else {
// ! 2. render结束,做一些检查
// The render completed.

// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}

// Check if something threw
if (exitStatus === RootErrored) {
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
renderWasConcurrent = false;
}
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes, NoLane);
ensureRootIsScheduled(root);
throw fatalError;
}

// !3. commit
// 我们现在有了一个一致的树。下一步要么是 commit,要么是,如果有什么被暂停了,就等待一段时间后再 commit。
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}

ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}

1. render阶段 - renderRootSync

进入 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
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
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// ! 1. 记录 render阶段 开始
const prevExecutionContext = executionContext;
executionContext |= RenderContext;

const prevDispatcher = pushDispatcher(root.containerInfo);
const prevCacheDispatcher = pushCacheDispatcher();

// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}

// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}

// ! 2. workInProgressTransitions赋值(用于dev环境)
workInProgressTransitions = getTransitionsForLanes(root, lanes);
// ! 3. 初始化
prepareFreshStack(root, lanes);
}

if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}

let didSuspendInShell = false;
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// The work loop is suspended. During a synchronous render, we don't
// yield to the main thread. Immediately unwind the stack. This will
// trigger either a fallback or an error boundary.
// TODO: For discrete and "default" updates (anything that's not
// flushSync), we want to wait for the microtasks the flush before
// unwinding. Will probably implement this using renderRootConcurrent,
// or merge renderRootSync and renderRootConcurrent into the same
// function and fork the behavior some other way.
const unitOfWork = workInProgress;
const thrownValue = workInProgressThrownValue;
switch (workInProgressSuspendedReason) {
case SuspendedOnHydration: {
// Selective hydration. An update flowed into a dehydrated tree.
// Interrupt the current render so the work loop can switch to the
// hydration lane.
resetWorkInProgressStack();
workInProgressRootExitStatus = RootDidNotComplete;
break outer;
}
case SuspendedOnImmediate:
case SuspendedOnData: {
if (!didSuspendInShell && getSuspenseHandler() === null) {
didSuspendInShell = true;
}
// Intentional fallthrough
}
default: {
// Unwind then continue with the normal work loop.
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
break;
}
}
}
// ! 4. workLoopSync
workLoopSync();
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
}
} while (true);
// Check if something suspended in the shell. We use this to detect an
// infinite ping loop caused by an uncached promise.
//
// Only increment this counter once per synchronous render attempt across the
// whole tree. Even if there are many sibling components that suspend, this
// counter only gets incremented once.
if (didSuspendInShell) {
root.shellSuspendCounter++;
}
// ! 5. 重置
// 重置Context的相关值,如currentlyRenderingFiber等
resetContextDependencies();
// 重置 executionContext
executionContext = prevExecutionContext;

popDispatcher(prevDispatcher);
popCacheDispatcher(prevCacheDispatcher);

if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
throw new Error(
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
}

if (enableSchedulingProfiler) {
markRenderStopped();
}

// Set this to null to indicate there's no in-progress render.
// 设置为null,表示没有进行中的render了
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;

// ! 6. 再遍历一遍更新队列
// It's safe to process the queue now that the render phase is complete.
finishQueueingConcurrentUpdates();

return workInProgressRootExitStatus;
}

1.1 记录 render 阶段 开始

1
2
const prevExecutionContext = executionContext;
executionContext |= RenderContext;

1.2 workInProgressTransitions赋值

1
workInProgressTransitions = getTransitionsForLanes(root, lanes);

1.3 prepareFreshStack 初始化

初始化 workInProgressRootworkInProgressworkInProgressRootRenderLanes 等值。

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
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;

const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe[incompatible-call] Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
const cancelPendingCommit = root.cancelPendingCommit;
if (cancelPendingCommit !== null) {
root.cancelPendingCommit = null;
cancelPendingCommit();
}

resetWorkInProgressStack();
workInProgressRoot = root; // FiberRoot

const rootWorkInProgress = createWorkInProgress(root.current, null); // Fiber

workInProgress = rootWorkInProgress; // Fiber
workInProgressRootRenderLanes = lanes;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressDeferredLane = NoLane;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
workInProgressRootDidIncludeRecursiveRenderUpdate = false;

// Get the lanes that are entangled with whatever we're about to render. We
// track these separately so we can distinguish the priority of the render
// task from the priority of the lanes it is entangled with. For example, a
// transition may not be allowed to finish unless it includes the Sync lane,
// which is currently suspended. We should be able to render the Transition
// and Sync lane in the same batch, but at Transition priority, because the
// Sync lane already suspended.

entangledRenderLanes = getEntangledLanes(root, lanes);

// 把concurrentQueues的内容添加到fiber的queue中,即给fiber的lanes、childLanes赋值
finishQueueingConcurrentUpdates();

return rootWorkInProgress;
}

1.4.1 workLoopSync

1
2
3
4
5
6
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

来自 renderRootSync,while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress === null。

所以同步渲染模式,一旦开始这个Fiber Reconciler协调流程,就会进入一直循环的逻辑,直到创建出完整的FiberTree,这个过程无法中断。

下面我们来看看并发渲染模式的方法。

1.4.2 workLoopConcurrent

1
2
3
4
5
6
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}

来自 renderRootConcurrent,可以看见和同步渲染模式基本一样,唯一的区别就是while循环的多了一个判断条件:当前程序运行时间是否小于帧间隔时间frameInterval【默认5ms】。

  • 如果小于:则返回false,代表还有剩余可执行时间。取反后为true,表示可以继续创建FiberTree的工作循环。
  • 如果大于:则返回true,代表没有剩余可执行时间。取反后为false,则while循环的条件将不再满足,会暂停创建FiberTree的工作,结束本次宏任务,剩下的工作会留待下一次宏任务再处理。

所以并发渲染模式和同步渲染模式的主要区别就是在这里:它可以中断FiberTree的创建过程,而同步渲染模式是无法中断这个过程的,它只能从开始到创建完成。

并且在这里我们也可以看出,它们的while循环里面都是执行了一个相同的performUnitOfWork方法,所以后面的逻辑也就没有什么模式的区分了,都是执行的一样的内容。

1.4.3 并发模式下暂停工作与恢复执行

packages\react-reconciler\src\ReactFiberWorkLoop.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 并发渲染模式
function renderRootConcurrent() {
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);

// 检查tree工作是否完成
if (workInProgress !== null) {
// Still work remaining.
return RootInProgress;
}
}

workLoopConcurrent 并发渲染的工作被中断后,就会退出 do while 循环,然后就会检查当前的 workInProgress 是否为 null,很明显被暂停的情况,workInProgress 是一定有值的,它的值为下一个即将处理的 Fiber 节点。

此时 workInProgress 不为 null,就会返回 RootInProgress 的渲染状态,表示还有剩下的工作。

这里 return 之后,就会回到 performConcurrentWorkOnRoot 方法之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function performConcurrentWorkOnRoot(root, didTimeout) {
// 返回未完成的渲染状态
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);

// 根据渲染状态 继续执行逻辑
if (exitStatus !== RootInProgress) {
// ...
}

// 渲染未完成的情况:
if (root.callbackNode === originalCallbackNode) {
// 渲染暂停的情况:继续返回performConcurrentWorkOnRoot,下一次宏任务继续处理
return performConcurrentWorkOnRoot.bind(null, root);
}
}
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
function workLoop() {
// ...
while (currentTask !== null) {

if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost()) ) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// ...

// callback就是performConcurrentWorkOnRoot函数
const continuationCallback = callback(didUserCallbackTimeout);
// continuationCallback 如果返回也是 performConcurrentWorkOnRoot函数
if (typeof continuationCallback === 'function') {
// 说明任务还未完成,将任务继续设置未当前任务的callback,等待下次继续执行
// 这里没有删除这个任务,则下次取出的第一个任务,还是这个任务,
currentTask.callback = continuationCallback;
}
}

if (currentTask !== null) {
// 还有工作,则会生成一个新的宏任务,在下次的宏任务中继续执行剩下的任务
return true;
}
else {
// ...
}
}

在workLoop方法之中,就会将返回的内容再次赋值给当前任务currentTask的callback属性,表示还有未完成的工作。workLoop方法中的while循环就会被中断,原因同样是shouldYieldToHost方法。

在workLoop方法最后就会判断当前任务currentTask是否为null,很明显被中断的情况,currentTask是有值的,所以就会返回true,这里会一直向上return:

最后会将结果返回performWorkUntilDeadline方法之中:

1
2
3
4
5
6
7
8
9
10
11
const performWorkUntilDeadline = () => {
// ...
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
// 此时hasMoreWork为true
if (hasMoreWork) {
// 如果还有任务,则又触发创建宏任务事件,生成新的宏任务,即在下一个消息事件继续执行任务
schedulePerformWorkUntilDeadline();
} else {
// ...
}
}

此时hasMoreWork为true,就会调用schedulePerformWorkUntilDeadline方法,生成一个新的宏任务,在下个宏任务中继续执行剩下的任务。

恢复执行的原理也很简单:在下次的宏任务中继续创建FiberTree的时候,因为workInProgress是一个全局变量,一直保存在内存之中,并且它内容为下一个即将处理的Fiber节点,所以下次再回到这里时,就会自动从这个Fiber节点开始继续执行剩下的创建工作。

1.5 performUnitOfWork

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
function performUnitOfWork(unitOfWork: Fiber): void {
// 缓存的上一次的Fiber
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);

// 开始工作
let next = beginWork(current, unitOfWork, entangledRenderLanes);

resetCurrentDebugFiberInDEV();
// ! 把pendingProps更新到memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;

/**
* 注意:当遍历到叶子元素,即没有子FiberNode时,performUnitOfWork会进入归的阶段。
* 这个阶段会调用completeUnitOfWork处理FiberNode,当某个FiberNode阶段执行completeUnitOfWork方法后,
* 如果存在其他的兄弟节点【FiberNode.sibling !== null】,会进入兄弟节点的归阶段,
* 如果不存在其他兄弟节点,则进入父节点的归阶段,递阶段和归阶段会交替进行直到HostFiber的归阶段
* 至此,render工作结束,Fiber树创建完成。
*
*/
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 如果不再产生新的work,那么当前work结束
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}

// 当前 Fiber 为null
ReactCurrentOwner.current = null;
}

Fiber 树结构

1
2
3
4
5
6
7
8
// index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// App.js

import MyFun from './views/MyFun';
import MyClass from './views/MyClass';

export default function App() {
console.log('App组件运行了')
return (
<div className="App">
<div>react源码调试</div>
<MyFun name='MyFun'></MyFun>
<MyClass name='MyClass'></MyClass>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  MyFun.js
import React from 'react'
import { useState } from 'react'

export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
return (
<div className='MyFun'>
<div>MyFun组件</div>
<div>state: {count}</div>
<div>name: {props.name}</div>
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MyClass.js
import React, { Component } from 'react';

export default class MyClass extends Component {
constructor(props) {
super(props)
console.log('MyClass组件运行了')
this.state = {
count: 1
}
}
componentDidMount() {
console.log('MyClass组件mount完成')
}
render() {
return (
<div className='MyClass'>
<div>MyClass组件</div>
<div>state: {this.state.count}</div>
<div>name: {this.props.name}</div>
</div>
);
}
}

alt text

1.5.1 beginWork

简单来说,就是生成 fiber 树。

packages\react-reconciler\src\ReactFiberBeginWork.js

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
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
let didReceiveUpdate: boolean = false;

function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// update更新流程
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 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 {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} 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;

if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}

// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
// 在进入开始阶段之前,清除待处理的更新优先级。
// TODO: 这假设我们即将评估组件并处理更新队列。然而,有一个例外情况:SimpleMemoComponent
// 有时会在开始阶段后退出。这表明我们应该将这个赋值移出常规路径,放到每个分支中。
workInProgress.lanes = NoLanes;

switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
// 函数组件
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,
renderLanes,
);
}
// 类组件
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,
renderLanes,
);
}
// 根Fiber(第一次都会走这里)
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostHoistable:
if (enableFloat && supportsResources) {
return updateHostHoistable(current, workInProgress, renderLanes);
}
case HostSingleton:
if (supportsSingletons) {
return updateHostSingleton(current, workInProgress, renderLanes);
}
// 原生标签,div span等标签
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
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,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
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,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
return updateLegacyHiddenComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(current, workInProgress, renderLanes);
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
return updateTracingMarkerComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
}

throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
1.5.1.1 updateHostRoot
  1. 更新当前 fiber,比如 props/state 更新,生命周期函数执行、Hooks 函数执行等。
  2. 返回一个下一个 fiber。

packages\react-reconciler\src\ReactFiberBeginWork.js

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
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {

pushHostRootContext(workInProgress);

// current是根fiber,current的的stateNode是FiberRoot
if (current === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}

// ! 1. 更新props、state、updateQuue、transition
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
// 把current上的updateQueue复用到 workInProgress 上一份。
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);

const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);

// ...

const nextChildren = nextState.element;

// ! 2. bailout or 协调子节点,如果新旧children相同,则会进入优化的逻辑,跳过本次reconcile协调流程,复用原来的节点内容
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

// ! 3. 返回子节点
return workInProgress.child;
}
1.5.1.2 updateHostComponent

packages\react-reconciler\src\ReactFiberBeginWork.js

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
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);

if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}

const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;

let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);

if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}

if (enableFormActions && enableAsyncActions) {
const memoizedState = workInProgress.memoizedState;
if (memoizedState !== null) {
// This fiber has been upgraded to a stateful component. The only way
// happens currently is for form actions. We use hooks to track the
// pending and error state of the form.
//
// Once a fiber is upgraded to be stateful, it remains stateful for the
// rest of its lifetime.
const newState = renderTransitionAwareHostComponentWithHooks(
current,
workInProgress,
renderLanes,
);

// If the transition state changed, propagate the change to all the
// descendents. We use Context as an implementation detail for this.
//
// This is intentionally set here instead of pushHostContext because
// pushHostContext gets called before we process the state hook, to avoid
// a state mismatch in the event that something suspends.
//
// NOTE: This assumes that there cannot be nested transition providers,
// because the only renderer that implements this feature is React DOM,
// and forms cannot be nested. If we did support nested providers, then
// we would need to push a context value even for host fibers that
// haven't been upgraded yet.
if (isPrimaryRenderer) {
HostTransitionContext._currentValue = newState;
} else {
HostTransitionContext._currentValue2 = newState;
}
if (enableLazyContextPropagation) {
// In the lazy propagation implementation, we don't scan for matching
// consumers until something bails out.
} else {
if (didReceiveUpdate) {
if (current !== null) {
const oldStateHook: Hook = current.memoizedState;
const oldState: TransitionStatus = oldStateHook.memoizedState;
// This uses regular equality instead of Object.is because we assume
// that host transition state doesn't include NaN as a valid type.
if (oldState !== newState) {
propagateContextChange(
workInProgress,
HostTransitionContext,
renderLanes,
);
}
}
}
}
}
}

markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
1.5.1.3 bailout

bailout阶段,返回null。

不会发生在组件初次渲染阶段,仅仅发生在组件更新阶段。当组件⼦节点没有发⽣变化,或者是被⼿动挡住(如类组件的 shouldComponentUpdate、memo 等),组件⼦节点不需要协调的时候。

1.5.1.4 协调子节点 reconcileChildren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// 组件初次挂载
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 组件更新
workInProgress.child = reconcileChildFibers(
workInProgress, // 父节点
current.child, // 旧的child
nextChildren, // 新的child
renderLanes, // 本次更新的lanes
);
}
}

其实最终执行的函数是同一个,都是 createChildReconciler,而 createChildReconciler 是一个 wrapper function。最终会返回内部的 reconcileChildFibers 函数。

1
2
3
export const reconcileChildFibers: ChildReconciler =
createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
1.5.1.5 reconcileChildFibers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// * 协调子节点,构建新的子fiber结构,并且返回新的子fiber
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null, // 老fiber的第一个子节点
newChild: any,
lanes: Lanes,
): Fiber | null {
// ...

const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
null, // debugInfo
);

// ...
return firstChildFiber;
}
1.5.1.6 reconcileChildFibersImpl
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
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber | null {
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
// 如果newChild是Fragment类型,且没有key,则直接协调其子节点
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

// 根据新的child类型,进行不同的处理
// 单个节点、数组、迭代器、promise、context处理,最常见的react-element元素类型:react中类组件,函数组件,普通dom组件都属于此类型
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
debugInfo,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
init(payload),
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
}
// 2,数组节点的处理,即多个子节点,比如div.App下面有三个子节点
// 循环创建多个子节点,最后返回第一个子节点,firstChild
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
}

if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
}

// Usables are a valid React node type. When React encounters a Usable in
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
// stack, then replay the component once the promise resolves.
//
// A difference from `use` is that React will keep unwrapping the value
// until it reaches a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
//
// The structure is a bit unfortunate. Ideally, we shouldn't need to
// replay the entire begin phase of the parent fiber in order to reconcile
// the children again. This would require a somewhat significant refactor,
// because reconcilation happens deep within the begin phase, and
// depending on the type of work, not always at the end. We should
// consider as an future improvement.
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
mergeDebugInfo(debugInfo, thenable._debugInfo),
);
}

if (newChild.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
debugInfo,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
}

// 3,处理文本子节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}

// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
1.5.1.6 reconcileSingleElement
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
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;

// 检查老的fiber单链表中是否有可以复用的节点
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 新老都是Fragment
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
coerceRef(returnFiber, child, existing, element);
existing.return = returnFiber;
return existing;
}
}
// 如果key相同,类型不相同。不再查找
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 这个老节点没法复用
deleteChild(returnFiber, child);
}
child = child.sibling;
}

// 1. 初次挂载 2. 没有找到可以复用的老节点
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
// 根据React-element元素对象,创建FiberNode节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(returnFiber, currentFirstChild, created, element);
created.return = returnFiber;
return created;
}
}
1.5.1.7 createFiberFromElement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type; // 存储原始的组件内容,比如function,class
const key = element.key;
const pendingProps = element.props; // 等待处理的,最新的props

// 创建Fiber节点: 根据元素的type ,针对组件来说:type值一般为class类 或者Function函数
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}

createFiberFromElement 方法很重要,react 所有 reactElement 元素都是通过这个方法来创建对应的 Fiber 节点。比如常见的 class组件,function组件,原生dom元素等。我们在项目中定义的组件和元素在被编译之后,最终在加载时都会通过jsxRuntime.jsx方法转换为一个reactElement元素对象。

1.5.1.8 createFiberFromTypeAndProps
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
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
let resolvedType = type;

// 根据元素的type进行不同的逻辑处理,class也是函数
if (typeof type === 'function') {
// 针对类组件的特殊处理
if (shouldConstruct(type)) {
// 根据type 设置fiberTag 为class组件
fiberTag = ClassComponent;
}
// 普通函数组件,没有更新tag【重要,伏笔】
} else if (typeof type === 'string') {
// 原生dom的处理
fiberTag = HostComponent;
} else {
// 其他type类型的处理
// ...
}

// 创建Fiber
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;

return fiber;
}

在react应用中我们定义的class组件,都是通过继承react的内部组件Component来定义的,所以通过它原型上的isReactComponent属性可以来区分当前组件是不是类组件,返回true则是类组件,返回false则是函数组件。

如果是类组件就会更新fiberTag为ClassComponent的值。

如果是函数组件,这里并没有更新fiberTag,所以函数组件的fiberTag还是为待定的IndeterminateComponent的值。

最后是针对普通的dom元素的处理,更新为HostComponent的值。

1.5.1.9 多子节点的创建 - reconcileChildrenArray
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
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber | null {
let resultingFirstChild: Fiber | null = null; // 存储新生成的child
let previousNewFiber: Fiber | null = null; // 上一个新建的child

let oldFiber = currentFirstChild; // 旧的Fiber节点
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// ! 1. 从左边往右遍历,比较新老节点,如果节点可以复用,继续往右,否则就停止
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

// !2.1 新节点没了,(老节点还有)。则删除剩余的老节点即可
// 0 1 2 3 4
// 0 1 2 3
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
// ! 2.2 (新节点还有),老节点没了
// 0 1 2 3 4
// 0 1 2 3 4 5
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}


// !2.3 新老节点都还有节点,但是因为老fiber是链表,不方便快速get与delete,
// ! 因此把老fiber链表中的节点放入Map中,后续操作这个Map的get与delete
// 0 1| 4 5
// 0 1| 7 8 2 3
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

// !3. 如果是组件更新阶段,此时新节点已经遍历完了,能复用的老节点都用完了,
// ! 则最后查找Map里是否还有元素,如果有,则证明是新节点里不能复用的,也就是要被删除的元素,此时删除这些元素就可以了
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}

if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
总结

从 hostFiber 根节点开始,react 的深度优先遍历算法,会一直创建到再无 Child 子节点。

此时叶子结点所对应的Fiber节点创建完成后,它会继续开启属于它的 beginWork 工作,来创建它的子节点,但是因为它已经没有子节点了,所以它最终会返回一个null。

然后一直向上return,直到回到performUnitOfWork方法中:将进入此 Fiber 节点的 completeWork 工作,也就是归的阶段

1.5.2 completeUnitOfWork

completeWork 代表一个 Fiber 节点的结束工作,completeUnitOfWork 是 completeWork 阶段的入口函数。简单来说,就是生成 dom 树。

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
function completeUnitOfWork(unitOfWork: Fiber): void {
// 取出当前的Fiber节点
let completedWork: Fiber = unitOfWork;
do {
// 取出节点对应的current
const current = completedWork.alternate;
// 取出节点的父级节点
const returnFiber = completedWork.return;

let next;
if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
next = completeWork(current, completedWork, entangledRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, entangledRenderLanes);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}

if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}

// 一个节点的completeWork工作完成后,寻找是否存在兄弟节点
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果兄弟节点存在,则设置为新的workInProgress,跳出当前函数,回到performUnitOfWork方法中
// 这将开始sibling兄弟节点的beginWork工作【自上而下】
workInProgress = siblingFiber;
return;
}
// 【自下而上的处理】
// 如果不存在兄弟节点,则将父级节点设置为completedWork,开始父级节点的completedWork工作
completedWork = returnFiber;
// 更新workInProgress,可能会开启新的beginWork工作
workInProgress = completedWork;
} while (completedWork !== null);

// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}

completeUnitOfWork方法中重点就是一个do while循环:completedWork !== null。这个循环的条件是completedWork不等于null,即它是一个有效的Fiber节点。

1.5.2.1 completeWork

根据tag值区别不同fiber
alt text

1.5.2.2 bubbleProperties

向上冒泡 subtreeFlags 和 childLanes

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
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;

let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;

if (!didBailout) {
// ? sy
// Bubble up the earliest expiration time.
// “向上冒泡”最早的过期时间
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
// ! 遍历completedWork的所有子节点
let child = completedWork.child;
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// ! 2. 将他们的subtreeFlags和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

actualDuration += child.actualDuration;

treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;

child.return = completedWork;
child = child.sibling;
}
}
// ! 子树的flags
completedWork.subtreeFlags |= subtreeFlags;
} else {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);

let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}

completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
// ! 遍历completedWork的所有子节点
while (child !== null) {
// ! 1. 将他们的lanes和childLanes合并到newChildLanes中
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);

// “静态”标志(Static flags)与它们所属的 Fiber 或 Hook 共享生命周期,因此即使在放弃(bailout)时,也应该将这些flags向上冒泡。
// 而其他所有flags仅在单次render + commit 的生命周期内存在,因此我们应该忽略它们。
// ! 2. 将他们的(subtreeFlags&StaticMask)和flags合并到subtreeFlags中
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;

// 更新return pointer以保持树的一致性。这被描述为一种代码异味(code smell),因为它假设commit阶段永远不会与 render 阶段并发。
// 在重构为交替模型(alternate model)时将会解决这个问题。
child.return = completedWork;
child = child.sibling;
}
}

completedWork.subtreeFlags |= subtreeFlags;
}
// ! 后代节点的lanes
completedWork.childLanes = newChildLanes;

return didBailout;
}
1.5.2.3 createInstance, 创建 DOM 节点,并添加到父节点中

如果组件是HostComponent,即原生标签

1
2
3
4
5
6
7
8
9
10
11
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 把DOM子节点添加到父DOM节点中去
appendAllChildren(instance, workInProgress, false, false);
// 当节点是原生标签,比如div、span等,会在这里添加到fiber属性stateNode上
workInProgress.stateNode = instance;
总结执行流程图

alt text

1.6 重置 workInProgressX

这里没有重置 workInProgress,因为 workInProgress 已经在 performUnitOfWork 阶段更新。

1
2
3
4
5
6
7
8
9
10
// ! 5. 重置
// 重置Context的相关值,如currentlyRenderingFiber等
resetContextDependencies();
// 重置 executionContext
executionContext = prevExecutionContext;

// Set this to null to indicate there's no in-progress render.
// 设置为null,表示没有进行中的render了
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;

1.7 再遍历一遍更新队列

1
finishQueueingConcurrentUpdates()

管理更新队列 finishQueueingConcurrentUpdates

finishQueueingConcurrentUpdatesconcurrentQueues 的内容添加到 fiberqueue 中,后续才是根据 VDOM 更新 DOM

render 阶段,有两处调⽤ finishQueueingConcurrentUpdates ,分别是

  1. render 开始的时候,在 prepareFreshStack 函数中;
    alt text
  2. render 结束的时候,最后再调⽤⼀遍。
    alt text

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js

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
export function finishQueueingConcurrentUpdates(): void {
const endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0; // 重置

concurrentlyUpdatedLanes = NoLanes; // 重置

let i = 0;
while (i < endIndex) {
const fiber: Fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
const queue: ConcurrentQueue = concurrentQueues[i];
concurrentQueues[i++] = null;
const update: ConcurrentUpdate = concurrentQueues[i];
concurrentQueues[i++] = null;
const lane: Lane = concurrentQueues[i];
concurrentQueues[i++] = null;

// 注意:这里构建完之后的fiber.updateQueue.shared.pending数据类型是Update,但是其实这里构建成了一个单向循环链表。
// 所以fiber.updateQueue.shared.pending其实是指最后一个update,它的next指向的是第一个update
if (queue !== null && update !== null) {
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;
}

if (lane !== NoLane) {
// 更新fiber.lanes
// 从当前节点开始,往上找到根节点,更新childLanes
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
}

markUpdateLaneFromFiberToRoot

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
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
update: ConcurrentUpdate | null,
lane: Lane,
): void {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// Walk the parent path to the root and update the child lanes.
// 从当前节点开始,往上找到根节点,更新childLanes
let isHidden = false;
let parent = sourceFiber.return;
let node = sourceFiber;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}

if (parent.tag === OffscreenComponent) {
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
if (
offscreenInstance !== null &&
!(offscreenInstance._visibility & OffscreenVisible)
) {
isHidden = true;
}
}

node = parent;
parent = parent.return;
}
}

处理更新队列 processUpdateQueue

这个函数⽤来处理更新队列。

processUpdateQueuebeginWork 阶段会被两个地⽅调⽤:

  1. updateHostRoot packages\react-reconciler\src\ReactFiberBeginWork.js

    1
    processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  2. updateClassInstance packages\react-reconciler\src\ReactFiberClassComponent.js

    在类组件的 mount、resumeMount、更新阶段,均会调⽤。

    1
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);

processUpdateQueue 流程

  1. 检查是否有 pending update。

    1. 如果有,将它们转移到 baseQueue。
    2. pending update 是个单向循环链表,转移到 单链表 firstBaseUpdate->…->lastBaseUpdate 中去。
  2. 遍历 queue,根据这些 update 计算出最后的结果

1
2
3
4
5
6
7
8
9
// 处理这个更新
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);
  1. 更新到 fiber 上
    1
    2
    workInProgress.lanes = newLanes;
    workInProgress.memoizedState = newState; // 更新状态

react源码分析(二)scheduleUpdateOnFiber调度更新

scheduleUpdateOnFiber 函数主要是触发一个在 Fiber 节点上的调度更新任务,⻚⾯初次渲染、类组件 setState/forceUpdate、函数组件 setState 都会⾛到更新,都会调⽤ scheduleUpdateOnFiber 函数。

如果以下源码没有特殊标记路径,那么路径都和 scheduleUpdateOnFiber ⼀样,即 packages/react-reconciler/src/ReactFiberWorkLoop.js

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
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
// ...

// 1. 标记根节点有一个 pending update,即待处理的更新。
markRootUpdated(root, lane);

if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// 这个更新是在渲染阶段期间发送的。
// 如果更新源自用户空间(除了本地hook update之外,hook update会被单独处理,不会到达这个函数),那么这是一个错误。
// 但是,有一些内部的 React 功能将这个作为实现细节,比如选择性hydration。
warnAboutRenderPhaseUpdatesInDEV(fiber);

// Track lanes that were updated during the render phase
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {

// ...

// 页面初次渲染时候,此时workInProgressRoot还是null
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root.
if ((executionContext & RenderContext) === NoContext) {
workInProgressRootInterleavedUpdatedLanes = mergeLanes(
workInProgressRootInterleavedUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
);
}
}

// 2. 开始调度
ensureRootIsScheduled(root);

if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode
) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy) {
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
} else {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
}
}

1. markRootUpdated

标记根节点有一个 pending update,即待处理的更新。

1
markRootUpdated(root, lane);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function markRootUpdated(root: FiberRoot, updatedLanes: Lanes) {
_markRootUpdated(root, updatedLanes);

if (enableInfiniteRenderLoopDetection) {
// ? sy
// Check for recursive updates
if (executionContext & RenderContext) {
workInProgressRootDidIncludeRecursiveRenderUpdate = true;
} else if (executionContext & CommitContext) {
didIncludeCommitPhaseUpdate = true;
}
// getRootForUpdatedFiber中也有这个检测
// 如果循环超过限制次数(50次),抛出错误。比如在类组件的render函数里执行setState
throwIfInfiniteUpdateLoopDetected();
}

_markRootUpdated 来自 packages\react-reconciler\src\ReactFiberLane.js 的 markRootUpdated

1
2
3
4
5
6
7
8
9
10
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
// 设置本次更新的优先级, 标记其有正在等待更新的任务,此时挂载的优先级将会后在后续被转换为调度优先级,方便调度程序的使用。
root.pendingLanes |= updateLane;

// 如果update是idle的,将不会处理它,因为我们直到所有常规update完成后才会处理idle更新;
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes; //【由于Suspense而挂起的update对应的lane集合】
root.pingedLanes = NoLanes; // 【由于请求成功,Suspense取消挂起的update对应的Lane集合】
}
}

markRootUpdated 主要做的就是设置本次更新的优先级,标记其有正在等待更新的任务,此时挂载的优先级将会后在后续被转换为调度优先级,方便调度程序的使用。

2. ensureRootIsScheduled(root)

每次 root: FiberRoot 接收 update 的时候,这个函数都会被调⽤。

  1. 确保 root 在 root 调度中
  2. 确保有⼀个待处理的微任务来处理根调度

packages\react-reconciler\src\ReactFiberRootScheduler.js

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
// 单链表
// 一般应用中,只有一个根节点,但是React支持多个根节点。
let firstScheduledRoot: FiberRoot | null = null;
let lastScheduledRoot: FiberRoot | null = null;
// 用于没有同步work的时候,快速退出flushSync
let mightHavePendingSyncWork: boolean = false;

export function ensureRootIsScheduled(root: FiberRoot): void {
// 把root添加到调度中
if (root === lastScheduledRoot || root.next !== null) {
// ? 后两次
// Fast path. This root is already scheduled.
// 已经添加到了调度中
} else {
// ? sy 前一次
if (lastScheduledRoot === null) {
// ? sy
firstScheduledRoot = lastScheduledRoot = root;
} else {
// 多个根节点
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}

// 每当root接收到update,我们将其设置为true,直到下次处理调度为止。如果为false,那么我们可以在不查看调度的情况下快速退出 flushSync。
mightHavePendingSyncWork = true;

// 在当前事件结束时,逐个检查每个root,并确保为每个root安排了正确优先级的任务。
if (__DEV__ && ReactCurrentActQueue.current !== null) {
// ...
} else {
if (!didScheduleMicrotask) {
// ? sy
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
}

if (!enableDeferRootSchedulingToMicrotask) {
// While this flag is disabled, we schedule the render task immediately
// instead of waiting a microtask.
// TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to
// unblock additional features we have planned.
scheduleTaskForRootDuringMicrotask(root, now());
}
}

2.1 scheduleImmediateTask

packages\react-reconciler\src\ReactFiberRootScheduler.js

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
function scheduleImmediateTask(cb: () => mixed) {
// TODO: Can we land supportsMicrotasks? Which environments don't support it?
// Alternatively, can we move this check to the host config?
if (supportsMicrotasks) {
// sy-
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// 在Safari中,附加一个iframe会强制微任务运行。
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
// 在渲染或提交过程中我们不支持运行回调函数,因此我们需要进行检查。
const executionContext = getExecutionContext();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// sy-no
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).
// 如果这种情况发生在render或commit阶段之外(例如在事件中),这仍然会过早地刷新回调函数。

// Intentionally using a macrotask instead of a microtask here. This is
// wrong semantically but it prevents an infinite loop. The bug is
// Safari's, not ours, so we just do our best to not crash even though
// the behavior isn't completely correct.
// 在这里故意使用宏任务而不是微任务。这在语义上是错误的,但它可以防止无限循环。
// 这个 bug 是 Safari 的问题,而不是我们的问题,所以我们尽力避免崩溃,即使行为并不完全正确。
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
return;
}
cb();
});
} else {
// If microtasks are not supported, use Scheduler.
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
}
}

2.1.1 scheduleMicrotask

packages\react-dom-bindings\src\client\ReactFiberConfigDOM.js

1
2
3
4
5
6
7
export const scheduleMicrotask: any =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof localPromise !== 'undefined'
? callback =>
localPromise.resolve(null).then(callback).catch(handleErrorInNextTick)
: scheduleTimeout; // TODO: Determine the best fallback here.

2.2 processRootScheduleInMicrotask

packages\react-reconciler\src\ReactFiberRootScheduler.js

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
function processRootScheduleInMicrotask() {
// 这个函数总是在microtask中被调用,它绝对不应该被同步调用。
didScheduleMicrotask = false;

// 我们将在遍历所有roots并调度它们时重新计算这个
mightHavePendingSyncWork = false;

const currentTime = now();

let prev = null;
let root = firstScheduledRoot;
while (root !== null) {

const next = root.next;

if (
currentEventTransitionLane !== NoLane &&
shouldAttemptEagerTransition()
) {
// A transition was scheduled during an event, but we're going to try to
// render it synchronously anyway. We do this during a popstate event to
// preserve the scroll position of the previous page.
upgradePendingLaneToSync(root, currentEventTransitionLane);
}

const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
// ? sy second
// 页面初次渲染,再执行这里。nextLanes=0
// This root has no more pending work. Remove it from the schedule. To
// guard against subtle reentrancy bugs, this microtask is the only place
// we do this — you can add roots to the schedule whenever, but you can
// only remove them here.

// Null this out so we know it's been removed from the schedule.
//root 没有更多pending work。从调度中移除它。为了防止微妙的重入bug,这个microtask是我们唯一执行此操作的地方 — 你可以随时将roots添加到调度中,但只能在这里将它们移除。
// 将其置null,以便我们知道它已从调度中移除
root.next = null;
if (prev === null) {
// ? sy
// This is the new head of the list
firstScheduledRoot = next;
} else {
prev.next = next;
}
if (next === null) {
// ? sy
// This is the new tail of the list
lastScheduledRoot = prev;
}
} else {
// ? sy first
// 页面初次渲染,先执行这里。nextLanes=32
// This root still has work. Keep it in the list.
prev = root;
if (includesSyncLane(nextLanes)) {
// ? sy-no
mightHavePendingSyncWork = true;
}
}
root = next;
}

currentEventTransitionLane = NoLane;

// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
// 在microtask结束时,flush任何pending的同步work。这必须放在最后,因为它执行实际的可能会抛出异常的渲染工作。
// onClick count
flushSyncWorkOnAllRoots();
}

2.3 scheduleTaskForRootDuringMicrotask

packages\react-reconciler\src\ReactFiberRootScheduler.js

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
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
// 这个函数总是在microtask中被调用,或者在渲染任务的最后,在我们将控制权交还给主线程之前被调用。它绝对不应该被同步调用。

// 检查是否有任何lanes被其他work饿死。如果是,将它们标记为过期,这样我们就知道下一个要处理的是它们。
markStarvedLanesAsExpired(root, currentTime);

// Determine the next lanes to work on, and their priority.
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();

// 基于root.pendingLanes,计算出本次更新的批次Lanes
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);

const existingCallbackNode = root.callbackNode;
// 如果优先级等于0 ,说明根节点没有可处理的回调,则退出任务调度
if (
// Check if there's nothing to work on
nextLanes === NoLanes ||
// If this root is currently suspended and waiting for data to resolve, don't
// schedule a task to render it. We'll either wait for a ping, or wait to
// receive an update.
//
// Suspended render phase
(root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
// Suspended commit phase
root.cancelPendingCommit !== null
) {
// Fast path: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}

// Schedule a new callback in the host environment.
if (includesSyncLane(nextLanes)) {
// sy- setState
// sy-no 初次渲染
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
// 同步工作始终在微任务结束时刷新,因此我们不需要安排额外的任务。
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);

if (
newCallbackPriority === existingCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-schedule
// on the `act` queue.
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
// The priority hasn't changed. We can reuse the existing task.
return newCallbackPriority;
} else {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}

// 定义一个【调度的优先级】,因为react事件优先级和scheduler的优先级不同,需要经过转换【即:事件优先级转换为调度优先级】
let schedulerPriorityLevel;
// lanes转换成事件优先级,匹配符合的优先级,然后赋值对应的scheduler的优先级
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority: // 32
// ? sy 页面初次渲染、transition(128)
schedulerPriorityLevel = NormalSchedulerPriority; // 3
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}

// ? sy 页面初次渲染
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}
}

2.3.1 markStarvedLanesAsExpired

检查是否有lanes挨饿,如果有,则标记他们过期,以便下次执行。详情参考lanes文章。

2.3.2 getWorkInProgressRoot

1
2
3
4
5
let workInProgressRoot: FiberRoot | null = null;

export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}

2.3.3 getWorkInProgressRootRenderLanes

1
2
3
export function getWorkInProgressRootRenderLanes(): Lanes {
return workInProgressRootRenderLanes;
}

2.3.4 getNextLanes

获取本次更新的lanes。详情参考lanes文章。

2.3.5 lanesToEventPriority

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function lanesToEventPriority(lanes: Lanes): EventPriority {
// 根据优先级最高的lane,返回对应的 EventPriority。这里对应Scheduler包中的优先级
const lane = getHighestPriorityLane(lanes);
if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
return DiscreteEventPriority;
}
if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
return ContinuousEventPriority;
}
if (includesNonIdleWork(lane)) {
return DefaultEventPriority;
}
return IdleEventPriority;
}

2.3.6 scheduleCallback

进入 schedule 调度器.

1
2
3
4
5
6
function scheduleCallback(
priorityLevel: PriorityLevel,
callback: RenderTaskFn,
) {
return Scheduler_scheduleCallback(priorityLevel, callback);
}

Scheduler_scheduleCallback 最终来自 packages\scheduler\src\forks\Scheduler.js 的 unstable_scheduleCallback,在 react调度器scheduler源码分析 会详细讲到。

2.3.7 performConcurrentWorkOnRoot

具体看 render 阶段文章。

1. 入口 - createRoot API

createRoot 允许在浏览器的 DOM 节点创建根节点以显示 React 组件。

1
2
3
4
import { createRoot } from "react-dom/client";
import jsx from "./pages/ExamplePage";
const root = createRoot(document.getElementById("root"));
root.render(jsx);

createRoot 接收两个参数 containeroptions,返回一个 RootType 类型,即 ReactDOMRoot 的实例。

packages\react-dom\src\client\ReactDOMRoot.js

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
export type RootType = {
render(children: ReactNodeList): void,
unmount(): void,
_internalRoot: FiberRoot | null,
};

export type CreateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
};

export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions, // 有两个参数可用,其余的属于非稳定值,不要使用
): RootType {
// 1. 检查container是否是DOM
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}

warnIfReactDOMContainerInDEV(container);

let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;

// 2. 检查options
if (options !== null && options !== undefined) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
}
if (
allowConcurrentByDefault &&
options.unstable_concurrentUpdatesByDefault === true
) {
concurrentUpdatesByDefaultOverride = true;
}
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
if (options.unstable_transitionCallbacks !== undefined) {
transitionCallbacks = options.unstable_transitionCallbacks;
}
}

// 3. 创建FiberRoot
const root: FiberRoot = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
// 4. 标记Container是根Fiber
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;

// comment nodes 已弃用,这里是为了兼容FB老代码 https://github.com/facebook/react/pull/24110
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
// 5. 从Container层监听listenToAllSupportedEvents
listenToAllSupportedEvents(rootContainerElement);

// 6. 返回ReactDOMRoot实例
return new ReactDOMRoot(root);
}

1. 检查container是否是DOM

如果不是,则抛出错误提示。

1
2
3
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}

2. 检查options

⽬前⽂档中有两个参数可⽤: onRecoverableError 与 identifierPrefix 。

但是源码中实际上还有⼀些 unstable 值,属于⾮稳定值,不要使⽤.

1
2
3
4
5
6
7
export type CreateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
};

3. createContainer 创建 FiberRoot, 即root

这⾥的 containerInfo 就是根 dom 节点。(就是那个 id 为 root 的div)。这个变量在 createRoot ⾥叫 container ,到这⾥换名成了 containerInfo

packages\react-reconciler\src\ReactFiberReconciler.js

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 function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
null,
);
}

createFiberRoot 创建并返回 FiberRoot

packages\react-reconciler\src\ReactFiberRoot.js

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
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
formState: ReactFormState<any, any> | null,
): FiberRoot {
// 1. fiberRoot 表示Fiber数据结构对象, 是Fiber数据结构中的最外层对象。
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
formState,
): any);
// 2. rootFiber表示组件挂载点对应的Fiber对象,比如React应用中默认的组件挂载点就是id为root的div。
const uninitializedFiber: Fiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
// 3. 循环构造root 和 uninitializedFiber
// fiberRoot包含rootFiber,在fiberRoot对象中有一个current属性,存储rootFiber。
root.current = uninitializedFiber;
// rootFiber指向fiberRoot,在rootFiber对象中有一个stateNode属性,指向fiberRoot。
uninitializedFiber.stateNode = root;

// 在React应用中,可支持多个根节点fiberRoot,每一个 fiberRoot 下可以有多个 rootFiber ,因为 render 方法是可以调用多次的。
// FiberRoot 会记录应用的更新信息,比如协调器在完成工作后,会将工作成果存储在fiberRoot中。

if (enableCache) {
// sy
const initialCache = createCache();
retainCache(initialCache);

// The pooledCache is a fresh cache instance that is used temporarily
// for newly mounted boundaries during a render. In general, the
// pooledCache is always cleared from the root at the end of a render:
// it is either released when render commits, or moved to an Offscreen
// component if rendering suspends. Because the lifetime of the pooled
// cache is distinct from the main memoizedState.cache, it must be
// retained separately.
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: (null: any), // not enabled yet
};
uninitializedFiber.memoizedState = initialState;
}

// 4. 初始化 initializeUpdateQueue
initializeUpdateQueue(uninitializedFiber);

return root;
}

3.1 实例化 FiberRootNode,创建 FiberRoot

packages\react-reconciler\src\ReactFiberRoot.js

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
function FiberRootNode(
this: $FlowFixMe,
containerInfo: any,
tag,
hydrate: any,
identifierPrefix: any,
onRecoverableError: any,
formState: ReactFormState<any, any> | null,
) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.cancelPendingCommit = null;
this.context = null;
this.pendingContext = null;
this.next = null;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.expirationTimes = createLaneMap(NoTimestamp);

this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.finishedLanes = NoLanes;
this.errorRecoveryDisabledLanes = NoLanes;
this.shellSuspendCounter = 0;

// 记录当前更新与其他更新之间的关联性,即它们之间存在依赖或相关性。
// 当一个更新被触发时,React 会根据其依赖关系计算出 entangledLanes,这些 entangledLanes 表示与当前更新相关联的其他更新的 Lanes。
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);

this.hiddenUpdates = createLaneMap(null);

this.identifierPrefix = identifierPrefix;
this.onRecoverableError = onRecoverableError;

if (enableCache) {
this.pooledCache = null;
this.pooledCacheLanes = NoLanes;
}

if (enableSuspenseCallback) {
this.hydrationCallbacks = null;
}

this.formState = formState;

this.incompleteTransitions = new Map();
if (enableTransitionTracing) {
this.transitionCallbacks = null;
const transitionLanesMap = (this.transitionLanes = []);
for (let i = 0; i < TotalLanes; i++) {
transitionLanesMap.push(null);
}
}

if (enableProfilerTimer && enableProfilerCommitHooks) {
this.effectDuration = 0;
this.passiveEffectDuration = 0;
}

if (enableUpdaterTracking) {
this.memoizedUpdaters = new Set();
const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
for (let i = 0; i < TotalLanes; i++) {
pendingUpdatersLaneMap.push(new Set());
}
}
}

3.2 createHostRootFiber 创建原生标签的根 Fiber

注意这⾥创建的是 Fiber 只是属于根部的 Fiber 。和 3.1 的 FiberRoot 不同,FiberRootFiber 是两个类型。

packages\react-reconciler\src\ReactFiber.js

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
export function createHostRootFiber(
tag: RootTag,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode;
if (isStrictMode === true) {
mode |= StrictLegacyMode | StrictEffectsMode;
}
if (
// We only use this flag for our repo tests to check both behaviors.
forceConcurrentByDefaultForTesting
) {
mode |= ConcurrentUpdatesByDefaultMode;
} else if (
// Only for internal experiments.
allowConcurrentByDefault &&
concurrentUpdatesByDefaultOverride
) {
mode |= ConcurrentUpdatesByDefaultMode;
}
} else {
mode = NoMode;
}

if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}

return createFiber(HostRoot, null, null, mode);
}
1
2
3
4
5
6
7
8
function createFiber(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
return new FiberNode(tag, pendingProps, key, mode);
}
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
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // 组件类型
this.key = key; // 组件props上的key
this.elementType = null; // ReactElement.type 组件的dom类型, 比如`div, p`
this.type = null; // 异步组件resolved之后返回的内容
this.stateNode = null; // 在浏览器环境对应dom节点

// Fiber
this.return = null; // 指向父节点
this.child = null; // 孩子节点
this.sibling = null; // 兄弟节点, 兄弟节点的return指向同一个父节点
this.index = 0;

this.ref = null;
this.refCleanup = null;

this.pendingProps = pendingProps; // 新的props
this.memoizedProps = null; // 上一次渲染完成的props
this.updateQueue = null; // 组件产生的update信息会放在这个队列
this.memoizedState = null; // 上一次渲染完成的state
this.dependencies = null;

this.mode = mode;

// Effects
this.flags = NoFlags; // 相当于之前的effectTag, 记录side effect类型
this.subtreeFlags = NoFlags;
this.deletions = null;

this.lanes = NoLanes; // 优先级相关
this.childLanes = NoLanes; // 优先级相关
this.alternate = null; // 对应的是current fiber
}

3.3 循环构造 root 和 uninitializedFiber

root.currentFiber
uninitializedFiber.stateNode 是根 FiberRoot

1
2
root.current = uninitializedFiber; // Fiber
uninitializedFiber.stateNode = root; // FiberRoot

3.4 初始化 initializeUpdateQueue

类似fiber,update queues也是成对出现的,一个已经完成的即对应目前页面,一个正在工作中的。

packages\react-reconciler\src\ReactFiberClassUpdateQueue.js

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
export type Update<State> = {
lane: Lane,

tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,

next: Update<State> | null,
};

export type SharedQueue<State> = {
pending: Update<State> | null, // 单向循环链表
lanes: Lanes,
// 如果类组件是Activity(以前叫OffScreen)的后代组件,需要延迟执行的其setState的callback。这里是先暂时收集,commit阶段提交
// Activity目前还是unstable,了解即可~
hiddenCallbacks: Array<() => mixed> | null,
};

export type UpdateQueue<State> = {
baseState: State,
// 单链表 firstBaseUpdate->...->lastBaseUpdate
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
callbacks: Array<() => mixed> | null,
};

// 这里初始化fiber.updateQueue
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};
fiber.updateQueue = queue;
}

4. markContainerAsRoot 标记 Container 是根 Fiber

这个函数给container根DOM节点赋值根Fiber

packages\react-dom-bindings\src\client\ReactDOMComponentTree.js

1
2
3
4
5
6
const randomKey = Math.random().toString(36).slice(2);
const internalContainerInstanceKey = '__reactContainer$' + randomKey;
// 标记根节点
export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
node[internalContainerInstanceKey] = hostRoot;
}

这个属性值在函数中用于 getClosestInstanceFromNode 和 getInstanceFromNode 中会用于根据根DOM去Fiber值。

对应的还有两个函数:

1
2
3
4
5
6
7
8
// 取消标记,在ReactDOMRoot.prototype.unmount函数里调用
export function unmarkContainerAsRoot(node: Container): void {
node[internalContainerInstanceKey] = null;
}
// 检查是否被标记为根节点
export function isContainerMarkedAsRoot(node: Container): boolean {
return !!node[internalContainerInstanceKey];
}

5. 从Container层监听listenToAllSupportedEvents

react事件比较复杂,后续事件文章展开写。

6. 返回ReactDOMRoot实例

1
return new ReactDOMRoot(root);

ReactDOMRoot 函数:

1
2
3
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}

2. 加载 - 执行 render 函数

createRoot 会返回 RootType 类型,即 ReactDOMRoot 的实例。

packages\react-dom\src\client\ReactDOMRoot.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export type RootType = {
render(children: ReactNodeList): void,
unmount(): void,
_internalRoot: FiberRoot | null,
};

function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};

updateContainer 源码:

packages\react-reconciler\src\ReactFiberReconciler.js

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
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ! 1. 获取current和lane
const current = container.current;
const lane = requestUpdateLane(current); // 页面初次渲染,defaultLane 32

// ! 2. 创建update
const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};

// 页面初次渲染,React18中已取消callback,只有老版本有效
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}

// ! 3. 将update入队管理
const root = enqueueUpdate(current, update, lane);

if (root !== null) {
// ! 4. 调度更新
scheduleUpdateOnFiber(root, current, lane);
// ! 5. 处理transitions,非紧急更新
entangleTransitions(root, current, lane);
}

return lane;
}

2.1 获取 current 和 lane

lane 是用于标识 update 优先级,一种标识 update 优先级的机制。每个 update 都会被分配一个或者多个 lane,以确定其在更新队列中的优先级顺序。

packages\react-reconciler\src\ReactFiberWorkLoop.js

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

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

// TODO: Move this type conversion to the event priority module.
// React外部的update,根据事件类型,向当前环境获取对应的优先级。
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
1.1.1 getCurrentEventPriority

packages\react-dom-bindings\src\client\ReactFiberConfigDOM.js

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

packages\react-reconciler\src\ReactEventPriorities.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export opaque type EventPriority = Lane;

// 优先级从上往下,越来越低
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;

let currentUpdatePriority: EventPriority = NoLane;

export function getCurrentUpdatePriority(): EventPriority {
return currentUpdatePriority;
}

export function setCurrentUpdatePriority(newPriority: EventPriority) {
currentUpdatePriority = newPriority;
}

2.2 创建update(createUpdate)

packages\react-reconciler\src\ReactFiberClassUpdateQueue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export type Update<State> = {
lane: Lane,

tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,

next: Update<State> | null,
};
export function createUpdate(lane: Lane): Update<mixed> {
const update: Update<mixed> = {
lane,

tag: UpdateState,
payload: null,
callback: null,

next: null,
};
return update;
}

2.3 update入队

createRoot(root).render() 阶段与类组件的 setStateforceUpdate 阶段最开始调⽤的是 ReactFiberClassUpdateQueue.js 中的 enqueueUpdate

packages\react-reconciler\src\ReactFiberClassUpdateQueue.js

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
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
): FiberRoot | null {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return null;
}

const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

// 类组件旧的生命周期相关的update
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
// This is an unsafe render phase update. Add directly to the update
// queue so we can process it immediately during the current render.
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;

// Update the childLanes even though we're most likely already rendering
// this fiber. This is for backwards compatibility in the case where you
// update a different component during render phase than the one that is
// currently renderings (a pattern that is accompanied by a warning).
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
// sy
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
}
1.3.1 enqueueConcurrentClassUpdate

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js

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
// ClassUpdate | HookUpdate;
export type ConcurrentUpdate = {
next: ConcurrentUpdate,
lane: Lane,
};
// ClassQueue | HookQueue;
type ConcurrentQueue = {
pending: ConcurrentUpdate | null,
};
// 如果渲染正在进行中,并且收到来自并发事件的更新,我们会等到当前的渲染结束(无论是完成还是被中断)之后再将其添加到 fiber 队列中。
// 将其推送到这个数组中,这样我们以后就可以访问queue、fiber、update等。
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;

let concurrentlyUpdatedLanes: Lanes = NoLanes;
export function enqueueConcurrentClassUpdate<State>(
fiber: Fiber,
queue: ClassQueue<State>,
update: ClassUpdate<State>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
// ! 1. update入队
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
// ! 2. 返回FiberRoot
return getRootForUpdatedFiber(fiber);
}
1.3.1.1 enqueueUpdate

update 存储到 concurrentQueues 中,虽然这个函数也叫 enqueueUpdate

这⾥的 enqueueUpdate 是数字式存储,并且是依次存储 fiberqueueupdatelane,下次依然这个顺序,最后执⾏处理的时候也要按照这个规律取值。

React 源码中其它地⽅这种结构都是对象式写法,这⾥⽐较罕⻅地写了这个结构。

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js

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 enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
// Don't update the `childLanes` on the return path yet. If we already in
// the middle of rendering, wait until after it has completed.
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;

concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

// The fiber's `lane` field is used in some places to check if any work is
// scheduled, to perform an eager bailout, so we need to update it immediately.
// TODO: We should probably move this to the "shared" queue instead.
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
1.3.1.2 getRootForUpdatedFiber 找到 FiberRoot

从 sourceFiber 开始,找到根 Fiber,返回其 stateNode,即 FiberRoot。

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js

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 getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
// 如果循环超过限制次数(50次),抛出错误。比如在类组件的render函数里执行setState
throwIfInfiniteUpdateLoopDetected();

// When a setState happens, we must ensure the root is scheduled. Because
// update queues do not have a backpointer to the root, the only way to do
// this currently is to walk up the return path. This used to not be a big
// deal because we would have to walk up the return path to set
// the `childLanes`, anyway, but now those two traversals happen at
// different times.
// TODO: Consider adding a `root` backpointer on the update queue.
// __DEV__,检查是否有未挂载的Fiber,如Can't perform a React state update on a component that hasn't mounted yet.
detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
let node = sourceFiber;
let parent = node.return;
// 循环往上查找,找到根节点
while (parent !== null) {
detectUpdateOnUnmountedFiber(sourceFiber, node);
node = parent;
parent = node.return;
}

// 根节点一定是HostRoot,返回根节点的stateNode,即FiberRoot
return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}

2.4 scheduleUpdateOnFiber 调度更新

调度 update。

具体查看 scheduleUpdateOnFiber调度更新 文章。

1
2
3
4
5
6
7
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
// ...
}

2.5 entangleTransitions 非紧急更新

处理非紧急更新 Transitions。查看后续文章。

组件的异步更新

effect

1
2
3
4
5
6
7
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, // fn:组件更新实际执行函数
NOOP,
() => queueJob(update), //scheduler: update: () => effect.run() ,相当于执行componentUpdateFn
instance.scope, // track it in component's effect scope
))

queueJob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const queue: SchedulerJob[] = []

const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

export function queueJob(job: SchedulerJob) {
if (
!queue.length ||
!queue.includes( // queue中是否已经存在相同job
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}

queueJob 执行主要是将 scheduler 添加到 queue 队列中,然后执行 queueFlush 函数。

queueFlush

1
2
3
4
5
6
7
8
9
function queueFlush() {
// isFlushing和isflushPending初始值都是false
// 说明当前没有flush任务在执行,也没有flush任务在等待执行
if (!isFlushing && !isFlushPending) {
// 初次执行queueFlush将isFlushPending设置为true,表示有flush任务在等待执行
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}

resolvedPromise 就是 promise.resolve(),flushJobs 被放到微任务队列中,等待所有同步任务执行完毕后执行,这样就可以保证flushJobs在一次组件更新中只执行一次。最后,更新 currentFlushPromise 以供 nextTick 使用。

flushJobs

当所有的同步scheduler执行完毕后,就会去处理微任务队列的任务,就会执行flushJobs回调函数。

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
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 组件更新的顺序是从父到子 因为父组件总是在子组件之前创建 所以它的渲染效果将具有更小的优先级
// 如果一个组件在父组件更新期间被卸载 则可以跳过它的更新
queue.sort(comparator)
// ...
// 先执行queue中的job 然后执行pendingPostFlushCbs中的job
// 这里可以实现watch中的 postFlush
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// job执行完毕后清空队列
flushIndex = 0
queue.length = 0

// 执行flushPostFlushCbs 此时组件已经更新完毕
flushPostFlushCbs(seen)

isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}

NextTick

vue3中的nextTick实现非常简单:

1
2
3
4
5
6
7
export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R,
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

这里的关键就是 currentFlushPromise,我们仔细看其实发现 currentFlushPromise 在 queueFlush 中就被赋值,它正是把执行组件更新函数的任务放入微队列中的promise,所以在此我们拿到 currentFlushPromise 正好把 nextTick 接收到的函数回调fn放到微队列中 flushJobs 的后面,等到 flushJobs 执行完成后组件也已经更新完毕,此时正是我们希望去执行 nextTick 回调的时机。

注意:我们知道在一个eventloop中,执行完微任务后才进行渲染更新,那nextTick能拿到最新的dom吗?答案是可以的,执行nextTick回调时候,dom已经被修改,只是还没渲染。我们运行下面的例子便可得到答案。

1
2
3
4
5
6
7
8
<div id="count">{{ count }}</div>

const count = ref(0);
count.value++;
nextTick(() => {
// 执行回调时候,虽然dom还没渲染,但dom已经被修改可以获取最新值
console.log('count', document.getElementById('count').innerText); // 1
});

总结

组件内当修改响应式数据后,组件更新函数会被放到queue中,然后注册一个微任务,这个微任务负责执行queue中的所有job,所以这时就算我们同步修改多次/多个响应式数据,同一个组件的更新函数只会被放入一次到queue中,nextTick的回调也会放入到微队列中 flushJobs 的后面,等到同步操作结束后才会去执行注册的微任务,组件更新函数才会被执行(nextTick在此后执行也会获取到最新的dom值),组件也会被更新。

计算属性:computed

Vue.js 3.0 提供了一个 computed 函数作为计算属性 API,我们先来看看它是如何使用的。

1
2
3
4
5
6
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
count.value++
console.log(plusOne.value) // 3

从代码中可以看到,我们先使用 ref API 创建了一个响应式对象 count,然后使用 computed API 创建了另一个响应式对象 plusOne,它的值是 count.value + 1,当我们修改 count.value 的时候, plusOne.value 就会自动发生变化。

注意,这里我们直接修改 plusOne.value 会报一个错误,这是因为如果我们传递给 computed 的是一个函数,那么这就是一个 getter 函数,我们只能获取它的值,而不能直接修改它。

在 getter 函数中,我们会根据响应式对象重新计算出新的值,这也就是它被叫做计算属性的原因,而这个响应式对象,就是计算属性的依赖。

当然,有时候我们也希望能够直接修改 computed 的返回值,那么我们可以给 computed 传入一个对象:

1
2
3
4
5
6
7
8
9
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0

在这个例子中,结合上述代码可以看到,我们给 computed 函数传入了一个拥有 getter 函数和 setter 函数的对象,getter 函数和之前一样,还是返回 count.value + 1;而 setter 函数,请注意,这里我们修改 plusOne.value 的值就会触发 setter 函数,其实 setter 函数内部实际上会根据传入的参数修改计算属性的依赖值 count.value,因为一旦依赖的值被修改了,我们再去获取计算属性就会重新执行一遍 getter,所以这样获取的值也就发生了变化。

好了,我们现在已经知道了 computed API 的两种使用方式了,接下来就看看它是怎样实现的:

computed

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
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
// getter 函数
let getter: ComputedGetter<T>
// setter 函数
let setter: ComputedSetter<T>

// 判断是否只有getter的情况,也就是上面第一种例子情况
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 生成computed实例
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}

return cRef as any
}

computed接收3个参数,主要看第一个参数,观察其类型,发现可以传两种参数:一种是一个getter函数,一种是个包含get、set的对象。
首先从getterOrOptions中确定getter、setter(如果getterOrOptions是个function,说明computed是不可写的,所以会将setter设置为一个空函数),确定好之后,创建ComputedRefImpl的实例,并将其返回。

ComputedRefImpl

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
export class ComputedRefImpl<T> {
public dep?: Dep = undefined // 存储effect的集合

private _value!: T
public readonly effect: ReactiveEffect<T>

public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false

public _cacheable: boolean

/**
* Dev only
*/
_warnRecursive?: boolean

constructor(
private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
// 创建effect
this.effect = new ReactiveEffect(
() => getter(this._value), // fn
// trigger
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
// 用于区分effect是否是computed
this.effect.computed = this
// this.effect.active与this._cacheable在SSR中为false
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}

get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
// 计算出的 ref 可能会被其他代理包装,例如 readonly(),所以转成原始类型
const self = toRaw(this)
// 当未缓存或者dirty也就是需要更新 并且值发生了改变,触发更新
if (
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!)) // 会触发一次run,相当于获取最新的依赖值,如果结果发生改变,触发更新
) {
triggerRefValue(self, DirtyLevels.Dirty)
}
// 进行依赖收集
trackRefValue(self)

// 如果 _dirtyLevel 高于 DirtyLevels.MaybeDirty_ComputedSideEffect, 触发更新
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}

set value(newValue: T) {
this._setter(newValue)
}

// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
get _dirty() {
return this.effect.dirty
}

set _dirty(v) {
this.effect.dirty = v
}
// #endregion
}

侦听器:watch

1.watch API 可以侦听一个 getter 函数,但是它必须返回一个响应式对象,当该响应式对象更新后,会执行对应的回调函数。

1
2
3
4
5
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
// 当 state.count 更新,会触发此回调函数
})

2.watch API 也可以直接侦听一个响应式对象,当响应式对象更新后,会执行对应的回调函数。

1
2
3
4
5
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (count, prevCount) => {
// 当 count.value 更新,会触发此回调函数
})

3.watch API 还可以直接侦听多个响应式对象,任意一个响应式对象更新后,就会执行对应的回调函数。

1
2
3
4
5
6
import { ref, watch } from 'vue'
const count = ref(0)
const count2 = ref(1)
watch([count, count2], ([count, count2], [prevCount, prevCount2]) => {
// 当 count.value 或者 count2.value 更新,会触发此回调函数
})

watch

1
2
3
4
5
6
7
function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>,
): WatchStopHandle {
return doWatch(source as any, cb, options)
}

我们看到watch内部调用了doWatch:

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
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{
immediate,
deep,
flush,
once,
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
// 如果有回调函数,并且只监听一次后移除watch
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
unwatch()
}
}

// 当前组件实例
const instance = currentInstance
const reactiveGetter = (source: object) =>
deep === true
? source // traverse will happen in wrapped getter below
: // for deep: false, only traverse root-level properties
traverse(source, deep === false ? 1 : undefined)

let getter: () => any
let forceTrigger = false
let isMultiSource = false

// 1. 根据不同source 创建不同的getter函数
// getter 函数与computed的getter函数作用类似
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// source是reactive对象时 自动开启deep=true
getter = () => reactiveGetter(source)
forceTrigger = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
// source是一个数组的时候,getter 遍历通过类型判断
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return reactiveGetter(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
// 如果有cb,代表source是个getter函数
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// 否则代表是watchEffect
// no cb -> simple effect
getter = () => {
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}

// 2.x array mutation watch compat
// 兼容vue2
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}

// 2. 深度监听
if (cb && deep) {
const baseGetter = getter
// traverse会递归遍历对象的所有属性 以达到深度监听的目的
getter = () => traverse(baseGetter())
}

// watch回调的第三个参数 可以用此注册一个cleanup函数 会在下一次watch cb调用前执行
// 常用于竞态问题的处理
let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}

// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager or sync flush
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
// we will also not call the invalidate callback (+ runner is not set up)
// ssr处理
}

// oldValue声明 多个source监听则初始化为数组
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 调度器调用时执行
const job: SchedulerJob = () => {
if (!effect.active || !effect.dirty) {
return
}
if (cb) {
// watch(source, cb)
// 获取newValue
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 调用cb 参数为newValue、oldValue、onCleanup
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup,
])
// 更新oldValue
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}

// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb

// 创建任务队列的调度回调scheduler
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步更新 即每次响应式数据改变都会回调一次cb
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// job放入pendingPostFlushCbs队列中
// pendingPostFlushCbs队列会在queue队列执行完毕后执行 函数更新effect通常会放在queue队列中
// 所以pendingPostFlushCbs队列执行时组件已经更新完毕
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
// 默认更新
// 原理在这:https://rile14929.github.io/zh-CN/vue3%E7%BB%84%E4%BB%B6%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E5%92%8CNextTick%E7%9A%84%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6.html
scheduler = () => queueJob(job)
}

// 创建effect effect.run的时候建立effect与getter内响应式数据的关系
const effect = new ReactiveEffect(getter, NOOP, scheduler)

const scope = getCurrentScope()
// 取消监听的函数
const unwatch = () => {
effect.stop()
if (scope) {
remove(scope.effects, effect)
}
}

// initial run
if (cb) {
if (immediate) {
// 立即执行一次job
job()
} else {
// 否则执行effect.run() 会执行getter 获取oldValue
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
effect.run()
}

if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
}

WatchEffect

watchEffect API 的作用是注册一个副作用函数,副作用函数内部可以访问到响应式对象,当内部响应式对象变化后再立即执行这个函数。

1
2
3
4
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => console.log(count.value))
count.value++

watchEffect 和前面的 watch API 主要有三点不同:

  1. 侦听的源不同 。watch API 可以侦听一个或多个响应式对象,也可以侦听一个 getter 函数,而 watchEffect API 侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象。

  2. 没有回调函数 。watchEffect API 没有回调函数,副作用函数的内部响应式对象发生变化后,会再次执行这个副作用函数。

  3. 立即执行 。watchEffect API 在创建好 watcher 后,会立刻执行它的副作用函数,而 watch API 需要配置 immediate 为 true,才会立即执行回调函数。

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
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase,
): WatchStopHandle {
return doWatch(effect, null, options)
}

export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions,
) {
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
)
}

export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions,
) {
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
)
}

简易版实现原理:

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
if (isFunction(source)) {
// no cb -> simple effect
getter = () => {
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
}

let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}

const effect = new ReactiveEffect(getter, NOOP, scheduler)

// initial run
if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
effect.run()
}

总结

以上就是计算属性computed和侦听器watch的实现原理,总的来说就是侦听器更适合用于在数据变化后执行某段逻辑的场景,而计算属性则用于一个数据依赖另外一些数据计算而来的场景。

文章里面涉及到任务队列异步更新的原理在这。

之前我们分析了 Composition API 的核心 setup 函数的实现,在 setup 函数中,我们多次使用一些 API 让数据变成响应式,那么这次我们就来深入学习和分析响应式内部的实现原理。

响应式它的本质是当数据变化后会自动执行某个函数,映射到组件的实现就是,当数据变化后,会自动触发组件的重新渲染。响应式是 Vue.js 组件化更新渲染的一个核心机制。

Vue.js 2.x的响应式:在内部通过 Object.defineProperty API 劫持数据的变化,在数据被访问的时候收集依赖,然后在数据被修改的时候通知依赖更新。我们用vue2官网的一张图可以直观地看清这个流程。

alt text

在 Vue.js 2.x 中,Watcher 就是依赖,有专门针对组件渲染的 render watcher。这里有两个流程,首先是依赖收集流程,组件在 render 的时候会访问模板中的数据,触发 getter 把 render watcher 作为依赖收集,并和数据建立联系;然后是派发通知流程,当我对这些数据修改的时候,会触发 setter,通知 render watcher 更新,进而触发了组件的重新渲染。

但是Object.defineProperty API 有一些缺点:包括不能监听对象属性新增和删除;初始化阶段递归执行 Object.defineProperty 带来的性能负担。

Vue.js 3.0 为了解决 Object.defineProperty 的这些缺陷,使用 Proxy API 重写了响应式部分,并独立维护和发布整个 reactivity 库,下面就来分析响应式部分的实现原理。

Reactive API

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
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}

function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 目标必须是对象或数组类型
if (!isObject(target)) {
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
// target 已经有对应的 Proxy 了
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
// 只有在白名单里的数据类型才能变成响应式
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 利用 Proxy 创建响应式
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}

可以看到,reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。

在这个过程中,createReactiveObject 函数主要做了以下几件事情。

  1. 函数首先判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target 必须是对象或者数组。
  2. 如果对一个已经是响应式的对象再次执行 reactive,还应该返回这个响应式对象
  3. 如果对同一个原始数据多次执行 reactive ,那么会返回相同的响应式对象
  4. 只有在白名单里的数据类型才能变成响应式,比如,带有 __v_skip 属性的对象、被冻结的对象,以及不在白名单内的对象如 Date 类型的对象实例是不能变成响应式的。

接下来,我们继续看 Proxy 处理器对象 mutableHandlers 的实现:

1
2
3
4
5
6
7
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}

它其实就是劫持了我们对 observed 对象的一些操作,比如:

访问对象属性会触发 get 函数;

设置对象属性会触发 set 函数;

删除对象属性会触发 deleteProperty 函数;

in 操作符会触发 has 函数;

通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。

因为无论命中哪个处理器函数,它都会做依赖收集和派发通知这两件事其中的一个,所以这里我只要分析常用的 get 和 set 函数就可以了。

依赖收集:get 函数

依赖收集发生在数据访问的阶段,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性被访问的时候就会执行 get 函数,我们来看一下 get 函数的实现流程:

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
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
if (key === ReactiveFlags.IS_REACTIVE) {
// 代理 __v_isReactive
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 代理 __v_isReadonly
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
// 代理 __v_isShallow
return shallow
} else if (key === ReactiveFlags.RAW) {
// 函数中判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。
if (
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}

// 是否是数组
const targetIsArray = isArray(target)

if (!isReadonly) {
// arrayInstrumentations 包含对数组一些方法修改的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}

const res = Reflect.get(target, key, receiver)
// 内置 Symbol key 不需要依赖收集
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}

// 依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

if (shallow) {
return res
}

if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}

// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}

return res
}

结合上面代码来看,get 函数主要做了四件事情,首先对特殊的 key 做了代理,这就是为什么我们在 createReactiveObject 函数中判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。

接着通过 Reflect.get 方法求值,如果 target 是数组且 key 命中了 arrayInstrumentations,则执行对应的函数,我们可以大概看一下 arrayInstrumentations 的实现:

arrayInstrumentations

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
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// toRaw 可以把响应式对象转成原始数据,this就是Reflect的receiver
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}
})
return instrumentations
}

也就是说,当 target 是一个数组的时候,我们去访问 target.includes、target.indexOf、 target.lastIndexOf 就会执行 arrayInstrumentations 代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。

当调用 target.push、target.pop、target.shift、target.unshift、target.splice 时候,由于都会访问.length导致收集了数组的length,在某种场景下造成无限循环,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
// https://github.com/vuejs/core/pull/2138

const arr = reactive([])

watchEffect(()=>{
arr.push(1)
})

watchEffect(()=>{
arr.push(2)
})

所以为避免此种情况发生,会暂停收集依赖,在执行完毕后,再恢复依赖收集。

回到 get 函数,第三步就是通过 Reflect.get 求值,然后会执行 track 函数收集依赖,我们稍后重点分析这个过程。

函数最后会对计算的值 res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。这么做是因为 Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。

整个 get 函数最核心的部分其实是执行 track 函数收集依赖,下面我们重点分析这个过程。

track

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
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 是否应该收集依赖 和 当前激活的 effect
if (shouldTrack && activeEffect) {
// 每个 target 对应一个 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep Map
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
}
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0,
)
}
}

export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
if (dep.get(effect) !== effect._trackId) {
// 收集当前激活的 effect 作为依赖
dep.set(effect, effect._trackId)
const oldDep = effect.deps[effect._depsLength]
if (oldDep !== dep) {
if (oldDep) {
cleanupDepEffect(oldDep, effect)
}
// 当前激活的 effect 收集 dep 集合作为依赖
effect.deps[effect._depsLength++] = dep
} else {
effect._depsLength++
}
if (__DEV__) {
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
}
}
}

分析track函数的实现前,我们先想一下要收集的依赖是什么,我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。

再来看实现,我们把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。为了方便理解,可以通过下图表示它们之间的关系:

../images/vue3响应式实现原理/image-20240316133230452

所以每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。

派发通知:set 函数

派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现:

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
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object,
) {
let oldValue = (target as any)[key]
if (!this._shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}

// 检查数组是否包含index或者对象是否有这个属性
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}

结合上述代码来看,set 函数的实现逻辑很简单,主要就做两件事情, 首先通过 Reflect.set 求值 , 然后通过 trigger 函数派发通知 ,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。

整个 set 函数最核心的部分就是 执行 trigger 函数派发通知 ,下面我们将重点分析这个过程。

我们先来看一下 trigger 函数的实现:

trigger

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
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
// 通过 targetMap 拿到 target 对应的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}

// 创建要执行的deps数组
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 清空数组或者map的时候触发所有key对应的的effect函数
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// set add delete操作 将key对应的effect函数添加到deps数组中
if (key !== void 0) {
deps.push(depsMap.get(key))
}

// also run for iteration key on ADD | DELETE | Map.SET
// 根据不同的操作类型push对应的dep
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}

pauseScheduling()
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0,
)
}
}
resetScheduling()
}
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 function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
// 执行triggerEffects,执行dep里所有的effect
for (const effect of dep.keys()) {
if (
effect._dirtyLevel < dirtyLevel &&
dep.get(effect) === effect._trackId
) {
const lastDirtyLevel = effect._dirtyLevel
effect._dirtyLevel = dirtyLevel
if (lastDirtyLevel === DirtyLevels.NotDirty) {
effect._shouldSchedule = true
if (__DEV__) {
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()
}
}
}
scheduleEffects(dep)
resetScheduling()
}

所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。

ref API

通过前面的分析,我们知道 reactive API 对传入的 target 类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 String、Number、Boolean)是不支持的。

但是有时候从需求上来说,可能我只希望把一个字符串变成响应式,却不得不封装成一个对象,这样使用上多少有一些不方便,于是 Vue.js 3.0 设计并实现了 ref API。

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
export function ref(value?: unknown) {
return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
// 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}

get value() {
// 依赖收集,key 为固定的 value
trackRefValue(this)
return this._value
}

set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 判断有变化后更新值
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 派发通知
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

可以看到,函数首先处理了嵌套 ref 的情况,如果传入的 rawValue 也是 ref,那么直接返回。

接着对 rawValue 做了一层转换,如果 rawValue 是对象或者数组类型,那么把它转换成一个 reactive 对象。

最后定义一个对 value 属性做 getter 和 setter 劫持的对象并返回,get 部分就是执行 track 函数做依赖收集然后返回它的值;set 部分就是设置新值并且执行 trigger 函数派发通知。

总结

最后通过一张图来看一下响应式的工作流程。

alt text

Vue.js 3.0 在响应式的实现思路和 Vue.js 2.x 差别并不大,主要就是 劫持数据的方式改成用 Proxy 实现 , 以及收集的依赖由 watcher 实例变成了组件副作用渲染函数

创建和设置组件实例

首先,我们来回顾一下组件的渲染流程:创建 vnode 、渲染 vnode 和生成 DOM。

其中渲染 vnode 的过程主要就是在挂载组件:

1
2
3
4
5
6
7
8
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

可以看到,这段挂载组件的代码主要做了三件事情:创建组件实例、设置组件实例和设置并运行带副作用的渲染函数。前面的文章没有仔细分析,所以我们重点分析前两个流程。

先看创建组件实例的流程,我们要关注 createComponentInstance 方法的实现:

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
function createComponentInstance (vnode, parent, suspense) {
// 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
// 组件唯一 id
uid: uid++,
// 组件 vnode
vnode,
// 父组件实例
parent,
// app 上下文
appContext,
// vnode 节点类型
type: vnode.type,
// 根组件实例
root: null,
// 新的组件 vnode
next: null,
// 子节点 vnode
subTree: null,
// 带副作用更新函数
update: null,
// 渲染函数
render: null,
// 渲染上下文代理
proxy: null,
// 带有 with 区块的渲染上下文代理
withProxy: null,
// 响应式相关对象
effects: null,
// 依赖注入相关
provides: parent ? parent.provides : Object.create(appContext.provides),
// 渲染代理的属性访问缓存
accessCache: null,
// 渲染缓存
renderCache: [],
// 渲染上下文
ctx: EMPTY_OBJ,
// data 数据
data: EMPTY_OBJ,
// props 数据
props: EMPTY_OBJ,
// 普通属性
attrs: EMPTY_OBJ,
// 插槽相关
slots: EMPTY_OBJ,
// 组件或者 DOM 的 ref 引用
refs: EMPTY_OBJ,
// setup 函数返回的响应式结果
setupState: EMPTY_OBJ,
// setup 函数上下文数据
setupContext: null,
// 注册的组件
components: Object.create(appContext.components),
// 注册的指令
directives: Object.create(appContext.directives),
// suspense 相关
suspense,
// suspense 异步依赖
asyncDep: null,
// suspense 异步依赖是否都已处理
asyncResolved: false,
// 是否挂载
isMounted: false,
// 是否卸载
isUnmounted: false,
// 是否激活
isDeactivated: false,
// 生命周期,before create
bc: null,
// 生命周期,created
c: null,
// 生命周期,before mount
bm: null,
// 生命周期,mounted
m: null,
// 生命周期,before update
bu: null,
// 生命周期,updated
u: null,
// 生命周期,unmounted
um: null,
// 生命周期,before unmount
bum: null,
// 生命周期, deactivate
da: null
// 生命周期 activated
a: null,
// 生命周期 render triggered
rtg: null,
// 生命周期 render tracked
rtc: null,
// 生命周期 error captured
ec: null,
// 派发事件方法
emit: null
}
// 初始化渲染上下文
instance.ctx = { _: instance }
// 初始化根组件指针
instance.root = parent ? parent.root : instance
// 初始化派发事件方法
instance.emit = emit.bind(null, instance)
return instance
}

创建好 instance 实例后,接下来就是设置它的一些属性。目前已完成了组件的上下文、根组件指针以及派发事件方法的设置。

接着是组件实例的设置流程,对 setup 函数的处理就在这里完成,我们来看一下 setupComponent 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setupComponent (instance, isSSR = false) {
const { props, children, shapeFlag } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & 4
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化 插槽
initSlots(instance, children)
// 设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
return setupResult
}

可以看到,我们从组件 vnode 中获取了 props、children、shapeFlag 等属性,然后分别对 props 和插槽进行初始化。

根据 shapeFlag 的值,我们可以判断这是不是一个有状态组件,如果是则要进一步去设置有状态组件的实例。

接下来我们要关注到 setupStatefulComponent 函数,它主要做了三件事:创建渲染上下文代理、判断处理 setup 函数和完成组件实例设置。它代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function setupStatefulComponent (instance, isSSR) {
const Component = instance.type
// 创建渲染代理的属性访问缓存
instance.accessCache = {}
// 1.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2.判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}
else {
// 完成组件实例设置
finishComponentSetup(instance)
}
}

创建渲染上下文代理

首先是创建渲染上下文代理的流程,它主要对 instance.ctx 做了代理。

在 Vue.js 2.x 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。举个例子:

1
2
3
4
5
6
7
8
9
10
<template>
<p>{{ msg }}</p>
</template>
<script>
export default {
data() {
msg: 1
}
}
</script>

在初始化组件的时候,data 中定义的 msg 在组件内部是存储在 this._data 上的,而模板渲染的时候访问 this.msg,实际上访问的是 this._data.msg,这是因为 Vue.js 2.x 在初始化 data 的时候,做了一层 proxy 代理。

到了 Vue.js 3.0,为了方便维护,我们把组件中不同状态的数据存储到不同的属性中,比如存储到 setupState、ctx、data、props 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。

明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: get、set 和 has。

当我们访问 instance.ctx 渲染上下文中的属性时,就会进入 get 函数。我们来看一下它的实现:

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
const PublicInstanceProxyHandlers = {
get ({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance
if (key[0] !== '$') {
// setupState / data / props / ctx
// 渲染代理的属性访问缓存中
const n = accessCache[key]
if (n !== undefined) {
// 从缓存中取
switch (n) {
case 0: /* SETUP */
return setupState[key]
case 1 :/* DATA */
return data[key]
case 3 :/* CONTEXT */
return ctx[key]
case 2: /* PROPS */
return props[key]
}
}
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache[key] = 0
// 从 setupState 中取数据
return setupState[key]
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 1
// 从 data 中取数据
return data[key]
}
else if (
type.props &&
hasOwn(normalizePropsOptions(type.props)[0], key)) {
accessCache[key] = 2
// 从 props 中取数据
return props[key]
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3
// 从 ctx 中取数据
return ctx[key]
}
else {
// 都取不到
accessCache[key] = 4
}
}
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// 公开的 $xxx 属性或方法
if (publicGetter) {
return publicGetter(instance)
}
else if (
// css 模块,通过 vue-loader 编译的时候注入
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])) {
return cssModule
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 用户自定义的属性,也用 `$` 开头
accessCache[key] = 3
return ctx[key]
}
else if (
// 全局定义的属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))) {
return globalProperties[key]
}
else if ((process.env.NODE_ENV !== 'production') &&
currentRenderingInstance && key.indexOf('__v') !== 0) {
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
// 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
`character and is not proxied on the render context.`)
}
else {
// 在模板中使用的变量如果没有定义,报警告
warn(`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`)
}
}
}
}

可以看到,函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种,setupState 就是 setup 函数返回的数据,ctx 包括了计算属性、组件方法和用户自定义的一些数据。

如果 key 不以 $ 开头,那么就依次判断 setupState、data、props、ctx 中是否包含这个 key,如果包含就返回对应值。注意这个判断顺序很重要,在 key 相同时它会决定数据获取的优先级。

再回到 get 函数中,我们可以看到这里定义了 accessCache 作为渲染代理的属性访问缓存,它具体是干什么的呢?组件在渲染时会经常访问数据进而触发 get 函数,这其中最昂贵的部分就是多次调用 hasOwn 去判断 key 在不在某个类型的数据中,但是在普通对象上执行简单的属性访问相对要快得多。所以在第一次获取 key 对应的数据后,我们利用 accessCache[key] 去缓存数据,下一次再次根据 key 查找数据,我们就可以直接通过 accessCache[key] 获取对应的值,就不需要依次调用 hasOwn 去判断了。这也是一个性能优化的小技巧。

如果 key 以 $ 开头,那么接下来又会有一系列的判断,首先判断是不是 Vue.js 内部公开的 $xxx 属性或方法(比如 $parent);然后判断是不是 vue-loader 编译注入的 css 模块内部的 key;接着判断是不是用户自定义以 $ 开头的 key;最后判断是不是全局属性。如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,第一种是在 data 中定义的数据以 $ 开头的警告,因为 $ 是保留字符,不会做代理;第二种是在模板中使用的变量没有定义的警告。

接下来是 set 代理过程,当我们修改 instance.ctx 渲染上下文中的属性的时候,就会进入 set 函数。我们来看一下 set 函数的实现:

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
const PublicInstanceProxyHandlers = {
set ({ _: instance }, key, value) {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 给 setupState 赋值
setupState[key] = value
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 给 data 赋值
data[key] = value
}
else if (key in instance.props) {
// 不能直接给 props 赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate prop "${key}". Props are readonly.`, instance)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
// 不能给 Vue 内部以 $ 开头的保留属性赋值
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`, instance)
return false
}
else {
// 用户自定义数据赋值
ctx[key] = value
}
return true
}
}

结合代码来看,函数主要做的事情就是对渲染上下文 instance.ctx 中的属性赋值,它实际上是代理到对应的数据类型中去完成赋值操作的。这里仍然要注意顺序问题,和 get 一样,优先判断 setupState,然后是 data,接着是 props。

最后是 has 代理过程,当我们判断属性是否存在于 instance.ctx 渲染上下文中时,就会进入 has 函数,这个在平时项目中用的比较少,同样来举个例子,当执行 created 钩子函数中的 ‘msg’ in this 时,就会触发 has 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
const PublicInstanceProxyHandlers = {
has
({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
// 依次判断
return (accessCache[key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
(type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key))
}
}

这个函数的实现很简单,依次判断 key 是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中,然后返回结果。

至此,我们就分析完创建上下文代理的过程,让我们回到 setupStatefulComponent 函数中,接下来分析第二个流程——判断处理 setup 函数。

判断处理 setup 函数

我们看一下整个逻辑涉及的代码:

1
2
3
4
5
6
7
8
9
10
11
// 判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}

如果我们在组件中定义了 setup 函数,接下来就是处理 setup 函数的流程,主要是三个步骤:创建 setup 函数上下文、执行 setup 函数并获取结果和处理 setup 函数的执行结果。接下来我们就逐个来分析。

首先判断 setup 函数的参数长度,如果大于 1,则创建 setupContext 上下文。

1
2
3
4
5
6
7
8
9
10
function createSetupContext(instance) {
return {
get attrs() {
return getAttrsProxy(instance)
},
slots: instance.slots,
emit: instance.emit,
expose,
}
}

这里返回了一个对象,包括 attrs、slots 和 emit 三个属性。setupContext 让我们在 setup 函数内部可以获取到组件的属性、插槽以及派发事件的方法 emit。

可以预见的是,这个 setupContext 对应的就是 setup 函数第二个参数,我们接下来看一下 setup 函数具体是如何执行的。

我们通过callWithErrorHandling来执行 setup 函数并获取结果,具体来看一下 callWithErrorHandling 函数的实现:

1
2
3
4
5
6
7
8
9
10
function callWithErrorHandling (fn, instance, type, args) {
let res
try {
res = args ? fn(...args) : fn()
}
catch (err) {
handleError(err, instance, type)
}
return res
}

可以看到,它其实就是对 fn 做的一层包装,内部还是执行了 fn,并在有参数的时候传入参数,所以 setup 的第一个参数是 instance.props,第二个参数是 setupContext。函数执行过程中如果有 JavaScript 执行错误就会捕获错误,并执行 handleError 函数来处理。

执行 setup 函数并拿到了返回的结果,那么接下来就要用 handleSetupResult 函数来处理结果。

我们详细看一下 handleSetupResult 函数的实现:

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
function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
// setup 返回渲染函数
instance.render = setupResult
}
else if (isObject(setupResult)) {
// 把 setup 返回结果变成响应式
instance.setupState = proxyRefs(setupResult)
}
finishComponentSetup(instance)
}

export function proxyRefs<T extends object>(
objectWithRefs: T,
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
},
}

可以看到,当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。

另外 setup 不仅仅支持返回一个对象,也可以返回一个函数作为组件的渲染函数。

在 handleSetupResult 的最后,会执行 finishComponentSetup 函数完成组件实例的设置,其实这个函数和 setup 函数的执行结果已经没什么关系了,提取到外面放在 handleSetupResult 函数后面执行更合理一些。

另外当组件没有定义的 setup 的时候,也会执行 finishComponentSetup 函数去完成组件实例的设置。

完成组件实例设置

接下来我们来看一下 finishComponentSetup 函数的实现:

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
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean,
) {
const Component = instance.type as ComponentOptions

// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
// 获取模板
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template ||
resolveMergedOptions(instance).template
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } =
Component
// 合并最终编译选项
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters,
},
compilerOptions,
),
componentCompilerOptions,
)
// 编译模板,生成render
Component.render = compile(template, finalCompilerOptions)
}
}
// 对于使用 with 块运行时编译的渲染函数,配置渲染上下文的代理RuntimeCompiledPublicInstanceProxyHandlers,是在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行的扩展,主要对has做处理优化。
if (installWithProxy) {
installWithProxy(instance)
}
}

// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
const reset = setCurrentInstance(instance)
pauseTracking()
try {
applyOptions(instance)
} finally {
resetTracking()
reset()
}
}
}

组件最终通过运行 render 函数生成子树 vnode,但是我们很少直接去编写 render 函数,通常会使用两种方式开发组件。

一个是通过编写组件的 template 模板去描述一个组件的 DOM 结构,或者是使用直接在组件的 render 方法内调用 h 方法来创建 vnode。

所以这里首先判断了组件的 render 函数是否存在,如果不存在说明使用的是 template 模版。

对于使用 with 块运行时编译的渲染函数,配置渲染上下文的代理RuntimeCompiledPublicInstanceProxyHandlers,基于PublicInstanceProxyHandlers,主要对has做处理

组件实例设置的最后一个流程——兼容 Vue.js 2.x 的 Options API。我们知道 Vue.js 2.x 是通过组件对象的方式去描述一个组件,Vue.js 3.0 仍然支持 Vue.js 2.x Options API 的写法,这主要就是通过 applyOptions方法实现的。

总结

alt text

这次主要详细分析了组件的初始化流程,主要包括创建组件实例和设置组件实例。通过进一步细节的深入,我们也了解了渲染上下文的代理过程;了解了 Composition API 中的 setup 启动函数执行的时机,以及如何建立 setup 返回结果和模板渲染之间的联系;了解了组件定义的模板或者渲染函数的标准化过程;了解了如何兼容 Vue.js 2.x 的 Options API。