react fiber 类组件状态更新篇

前言

本文仅在局部层面上解释类组件的状态更新机制,并没有对函数组件的 hooks、fiber 优先级等作说明。

Update 及 UpdateQueue

Update

fiber reconciler 将 fiber 的状态更新抽象为 Update 单向链表:

  • tag:更新类型,UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
  • payload:状态变更函数或新状态本身
  • callback:回调,作用于 fiber.effectTag,并将 callback 作为 side-effects 回调
  • expirationTime:deadline 时间,未到该时间点,不予更新
  • suspenseConfig:suspense 配置
  • next:指向下一个 Update
  • priority:仅限于 dev 环境

createUpdate 函数用于创建 Update。getStateFromUpdate 函数用于通过 Update 获取新的 fiber 状态,其处理方式基于 tag 类型。如 tag 为 UpdateState 时,getStateFromUpdate 将取用更新前的 state 值,并混入 payload 返回值或 payload 本身,作为新的 state 值返回。“payload 返回值”指的是 payload 本身是一个函数,它会以组件实例作为上下文,并以 prevState、nextProps 作为参数。getStateFromUpdate 依循 tag 值的处理机制如下:

  • UpdateState:基于 prevState 以及 payload 增量更新
  • ReplaceState:基于 payload 全量更新
  • ForceUpdate:state 值依旧取 prevState,同时 hasForceUpdate 会被置为 true。
  • CaptureUpdate:将 fiber.effectTag 置为

我们可以看到,虽然 Update 表现为链表形式,可以处理多个 state 更新作业,但是 getStateFromUpdate 与 fiber 任务的优先级、side-effects 机制均无关联。

UpdateQueue

react fiber 搜罗整理篇提到的,render 阶段的 state 更新作业可以被丢弃。在实现上,fiber reconciler 使用 UpdateQueue 存储 Update 更新队列。更新队列有两条,baseQueue 执行中的更新队列,pendingQueue(即 shared.pending)待执行的更新队列。因为 Update 表现为环状单向链表,baseQueue、pendingQueue 均存储单向链表的尾节点。丢弃更新作业的实现在于,将 pendingQueue 复制给 baseQueue,丢弃之前的 baseQueue(current fiber 和 work-in-progress fiber 均会重置 baseQueue)。UpdateQueue 的数据结构如下:

  • baseState:先前的状态,作为 payload 函数的 prevState 参数
  • baseQueue:存储执行中的更新任务 Update 队列,尾节点存储形式
  • shared:以 pending 属性存储待执行的更新任务 Update 队列,尾节点存储形式
  • effects:side-effects 队列,commit 阶段执行

fiber reconciler 会在挂载组件时调用 initializeUpdateQueue 函数初始化 fiber 节点的 updateQueue 队列。只有挂载的组件才会有有效的状态更新,卸载的组件没必要使用 updateQueue 属性。enqueueUpdate 函数将 update 添加到 pendingQueue 队列中,典型如类组件在 setState 方法调用期间将 update 添加到 pendingQueue 中。

processUpdateQueue 函数用于应用 Update 队列完成状态更新。其流程如:

  1. 丢失原先的 baseQueue,将 pendingQueue 复制给 baseQueue,作为执行中的 Update 队列。
  2. 启用 while 循环处理 Update 队列。如优先级足够,获取最新的状态值;如不够,添加到 newBaseQueue 队列,等待下次处理。至于为什么要使用 while 循环?是因为函数式组件可能会同步使用多个 useState 更新状态。特殊的,对于优先级不足的任务,其所处理的 prevState 是前一个任务更新后的状态值,如官方注释所示;当 baseQueue 已清空,while 循环会继续遍历 pendingQueue 并应用更新。
  3. while 循环结束后,如 newBaseQueue 队列非空,将其作为新的 baseQueue 以备更新;如为空,更新 baseState。
  4. 最后更新 work-in-progress fiber 的 memoizedState 等属性。
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
function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
hasForceUpdate = false;
// 丢弃原先的更新任务 baseQueue,将 pendingQueue 复制给 baseQueue
let baseQueue = queue.baseQueue;
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
let baseFirst = baseQueue.next;
let pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
baseQueue = pendingQueue;
queue.shared.pending = null;
const current = workInProgress.alternate;
if (current !== null) {
const currentQueue = current.updateQueue;
if (currentQueue !== null) {
currentQueue.baseQueue = pendingQueue;
}
}
}
// These values may change as we process the queue.
if (baseQueue !== null) {
let first = baseQueue.next;
let newState = queue.baseState;
let newExpirationTime = NoWork;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
if (first !== null) {
let update = first;
do {
// 优先级不足,将 update 添加到 newBaseQueue 队列中
// newBaseState 更新为前一个 update 任务的结果
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
const clone: Update<State> = {
expirationTime: update.expirationTime,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (updateExpirationTime > newExpirationTime) {
newExpirationTime = updateExpirationTime;
}
} else {
// 如果 update 在优先级不足的 update 之后,将其拷贝到 newBaseQueue 队列中
if (newBaseQueueLast !== null) {
const clone: Update<State> = {
expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
suspenseConfig: update.suspenseConfig,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Mark the event time of this update as relevant to this render pass.
// TODO: This should ideally use the true event time of this update rather than
// its priority which is a derived and not reverseable value.
// TODO: We should skip this update if it was already committed but currently
// we have no way of detecting the difference between a committed and suspended
// update here.
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);

// 包含 callback 回调,更新 fiber.effectTag、baseQueue.effects
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
let effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
}

update = update.next;
if (update === null || update === first) {
pendingQueue = queue.shared.pending;
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.
update = baseQueue.next = pendingQueue.next;
pendingQueue.next = first;
queue.baseQueue = baseQueue = pendingQueue;
queue.shared.pending = null;
}
}
} while (true);
}
// 如果没有优先级不足的 Update,更新 baseState
if (newBaseQueueLast === null) {
newBaseState = newState;
// 如果有优先级不足的 Update,使这些 Update 首尾相连
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
queue.baseState = ((newBaseState: any): State);// 最新状态
queue.baseQueue = newBaseQueueLast;// 优先级不足的 update 队列
// 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.
markUnprocessedUpdateTime(newExpirationTime);
workInProgress.expirationTime = newExpirationTime;
workInProgress.memoizedState = newState;
}
}

类组件状态更新流程

类组件状态更新按如下 4 种场景处理:

  1. 当组件挂载时,通过 initializeUpdateQueue 函数初始化 updateQueue 队列。
  2. 当 state 变更或强制渲染时,通过 enqueueUpdate 函数将 update 添加到 pendingQueue 队列。
  3. 在 render 阶段,cloneUpdateQueue 函数能把 current fiber 中的 updateQueue 复制给 work-in-progress fiber。这样就如官方注释所说,current fiber 和 work-in-progress 会持有相同的 updateQueue。cloneUpdateQueue 函数执行后,就会调用 processUpdateQueue 获取最新状态。
  4. 在 commit 阶段,通过 commitUpdateQueue 函数执行 side-effects 回调。

其中,render 阶段和 commit 阶段均通过 scheduleWork(即 scheduleUpdateOnFiber) 启动。这里仅简要说明 scheduleWork 的内部表现:

  1. 使用 react scheduler 再解析篇提到的 unstable_scheduleCallback 调度任务,以整个渲染流程作为单个任务(表现为 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot,内容包含react fiber 搜罗整理篇提到的 work-loop 大循环、 commitRootImpl 提交流程。届时 state 更新在 performSyncWorkOnRoot 等函数中表现为 batch 批量任务);
  2. commitRootImpl 通过 Scheduler_runWithPriority 执行,因此在 scheduler 调度机制之外,不可打断。

setState

当类组件通过 constructClassInstance 函数实例化期间,组件实例的 updater 属性即会赋值为 classComponentUpdater。组件实例的 setState 方法最终会触发 classComponentUpdater.enqueueSetState 方法的调用。其原理如下:

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
// react 包下 ReactBaseClasses.js 文件
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
// react-reconciler 包下 ReactFiberClassComponent.js 文件
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
enqueueUpdate(fiber, update);// 添加到 pendingQueue
scheduleWork(fiber, expirationTime);// 调度工作
},
enqueueReplaceState(inst, payload, callback) {/** **/},
enqueueForceUpdate(inst, callback) {/** **/},
};

processUpdateQueue

对于类组件,processUpdateQueue 会在以下场景中执行:

  • mountClassInstance 函数挂载组件时,以及该函数执行到 componentWillMount 生命周期时
  • resumeMountClassInstance 函数复用组件时
  • updateClassInstance 函数更新组件时

错误捕获及 Suspense

在执行 work-loop 期间,fiber reconciler 会对捕获的错误进行处理,如这里,或这里React Suspense 源码解读指出,react 先抛出错误,然后在 completeWork 执行完成之前捕获错误,并添加到 updateQueue 队列中。这里仅贴示 throwException 函数所用到的 createClassErrorUpdate 代码,用于将错误处理制作成 update。据此可以发现,getDerivedStateFromError、componentDidCatch 生命周期均基于构建 update 的方式得到调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
// 通过 update 执行 getDerivedStateFromError
const update = createUpdate(expirationTime, null);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === 'function') {
const error = errorInfo.value;
update.payload = () => {
logError(fiber, errorInfo);
return getDerivedStateFromError(error);
};
}

const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
if (typeof getDerivedStateFromError !== 'function') {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
// This gets reset before we yield back to the browser.
// TODO: Warn in strict mode if getDerivedStateFromError is
// not defined.
markLegacyErrorBoundaryAsFailed(this);

// Only log here if componentDidCatch is the only error boundary method defined
logError(fiber, errorInfo);
}

// 通过 side-effects 执行 componentDidCatch 生命周期
const error = errorInfo.value;
const stack = errorInfo.stack;
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
};
}
return update;
}

特殊的,对于使用 React.Suspense 包裹的懒加载组件,fiber reconciler 会将 thenable 形式的懒加载函数抛出,以此进入错误处理;随后在 throwException 函数处理过程,thenable 会被添加到 updateQueue 队列中。

1
2
3
4
5
6
7
function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
initializeLazyComponentType(lazyComponent);
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;// 即懒加载函数
}
return lazyComponent._result;// 懒加载成功或失败结果
}

如上,错误捕获及 Suspense 处理都是在 work-loop 循环抛出异常后,再行更新 updateQueue 队列。既然已经退出了 work-loop 循环,这时 updateQueue 队列就需要在下次重绘时执行。throwException 函数注释部分指出,fiber reconciler 会通过 attachPingListener 函数重新 restart 渲染流程。