React状态更新
几个关键节点
在开始学习前,我们先了解源码中几个关键节点(即几个关键函数的调用)。通过这章的学习,我们会将这些关键节点的调用路径串起来。
先从我们所熟知的概念开始。
render阶段的开始
render阶段
开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新。
commit阶段的开始
commit阶段
开始于commitRoot
方法的调用。其中rootFiber
会作为传参。
我们已经知道,render阶段
完成后会进入commit阶段
。让我们继续补全从触发状态更新
到render阶段
的路径。
触发状态更新(根据场景调用不同方法)
|
|
v
?
|
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
commit阶段(`commitRoot`)
创建Update对象
在React
中,有如下方法可以触发状态更新(排除SSR
相关):
- ReactDOM.render
- this.setState
- this.forceUpdate
- useState
- useReducer
这些方法调用的场景各不相同,他们是如何接入同一套状态更新机制呢?
答案是:每次状态更新
都会创建一个保存更新状态相关内容的对象,我们叫他Update
。在render阶段
的beginWork
中会根据Update
计算新的state
。
我们会在下一节详细讲解Update
。
从fiber到root
现在触发状态更新的fiber
上已经包含Update
对象。
我们知道,render阶段
是从rootFiber
开始向下遍历。那么如何从触发状态更新的fiber
得到rootFiber
呢?
答案是:调用markUpdateLaneFromFiberToRoot
方法。
你可以从这里 (opens new window)看到
markUpdateLaneFromFiberToRoot
的源码
该方法做的工作可以概括为:从触发状态更新的fiber
一直向上遍历到rootFiber
,并返回rootFiber
。
由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber
的优先级。
调度更新
现在我们拥有一个rootFiber
,该rootFiber
对应的Fiber树
中某个Fiber节点
包含一个Update
。
接下来通知Scheduler
根据更新的优先级,决定以同步还是异步的方式调度本次更新。
这里调用的方法是ensureRootIsScheduled
。
以下是ensureRootIsScheduled
最核心的一段代码:
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
你可以从这里 (opens new window)看到
ensureRootIsScheduled
的源码
其中,scheduleCallback
和scheduleSyncCallback
会调用Scheduler
提供的调度方法根据优先级
调度回调函数执行。
可以看到,这里调度的回调函数为:
performSyncWorkOnRoot.bind(null, root);
performConcurrentWorkOnRoot.bind(null, root);
即render阶段
的入口函数。
至此,状态更新
就和我们所熟知的render阶段
连接上了。
Update的分类
我们先来了解Update
的结构。
首先,我们将可以触发更新的方法所隶属的组件分类:
- ReactDOM.render —— HostRoot
- this.setState —— ClassComponent
- this.forceUpdate —— ClassComponent
- useState —— FunctionComponent
- useReducer —— FunctionComponent
可以看到,一共三种组件(HostRoot
| ClassComponent
| FunctionComponent
)可以触发更新。
由于不同类型组件工作方式不同,所以存在两种不同结构的Update
,其中ClassComponent
与HostRoot
共用一套Update
结构,FunctionComponent
单独使用一种Update
结构。
虽然他们的结构不同,但是他们工作机制与工作流程大体相同。
Update的结构
ClassComponent
与HostRoot
(即rootFiber.tag
对应类型)共用同一种Update结构
。
对应的结构如下:
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
Update
由createUpdate
方法返回,你可以从这里 (opens new window)看到createUpdate
的源码
字段意义如下:
-
eventTime:任务时间,通过
performance.now()
获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。 -
lane:优先级相关字段。当前还不需要掌握他,只需要知道不同
Update
优先级可能是不同的。 -
suspenseConfig:
Suspense
相关,暂不关注。 -
tag:更新的类型,包括
UpdateState
|ReplaceState
|ForceUpdate
|CaptureUpdate
。 -
payload:更新挂载的数据,不同类型组件挂载的数据不同。对于
ClassComponent
,payload
为this.setState
的第一个传参。对于HostRoot
,payload
为ReactDOM.render
的第一个传参。 -
callback:更新的回调函数。
-
next:与其他
Update
连接形成链表。
Update与Fiber的联系
我们发现,Update
存在一个连接其他Update
形成链表的字段next
。联系React
中另一种以链表形式组成的结构Fiber
,他们之间有什么关联么?
答案是肯定的。
Fiber节点
组成Fiber树
,页面中最多同时存在两棵Fiber树
:
- 代表当前页面状态的
current Fiber树
- 代表正在
render阶段
的workInProgress Fiber树
类似Fiber节点
组成Fiber树
,Fiber节点
上的多个Update
会组成链表并被包含在fiber.updateQueue
中。
什么情况下一个Fiber节点会存在多个Update?
你可能疑惑为什么一个Fiber节点
会存在多个Update
。这其实是很常见的情况。
在这里介绍一种最简单的情况:
onClick() {
this.setState({
a: 1
})
this.setState({
b: 2
})
}
在一个ClassComponent
中触发this.onClick
方法,方法内部调用了两次this.setState
。这会在该fiber
中产生两个Update
。
Fiber节点
最多同时存在两个updateQueue
:
current fiber
保存的updateQueue
即current updateQueue
workInProgress fiber
保存的updateQueue
即workInProgress updateQueue
在commit阶段
完成页面渲染后,workInProgress Fiber树
变为current Fiber树
,workInProgress Fiber树
内Fiber节点
的updateQueue
就变成current updateQueue
。
updateQueue
ClassComponent
与HostRoot
使用的UpdateQueue
结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
UpdateQueue
由initializeUpdateQueue
方法返回,你可以从这里 (opens new window)看到initializeUpdateQueue
的源码
字段说明如下:
-
baseState:本次更新前该
Fiber节点
的state
,Update
基于该state
计算更新后的state
。 -
firstBaseUpdate
与lastBaseUpdate
:本次更新前该Fiber节点
已保存的Update
。以链表形式存在,链表头为firstBaseUpdate
,链表尾为lastBaseUpdate
。之所以在更新产生前该Fiber节点
内就存在Update
,是由于某些Update
优先级较低所以在上次render阶段
由Update
计算state
时被跳过。 -
shared.pending
:触发更新时,产生的Update
会保存在shared.pending
中形成单向环状链表。当由Update
计算state
时这个环会被剪开并连接在lastBaseUpdate
后面。 -
effects:数组。保存
update.callback !== null
的Update
。
例子
updateQueue
相关代码逻辑涉及到大量链表操作,比较难懂。在此我们举例对updateQueue
的工作流程讲解下。
假设有一个fiber
刚经历commit阶段
完成渲染。
该fiber
上有两个由于优先级过低所以在上次的render阶段
并没有处理的Update
。他们会成为下次更新的baseUpdate
。
我们称其为u1
和u2
,其中u1.next === u2
。
fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;
u1.next === u2;
我们用-->
表示链表的指向:
fiber.updateQueue.baseUpdate: u1 --> u2
现在我们在fiber
上触发两次状态更新,这会先后产生两个新的Update
,我们称为u3
和u4
。
每个 update
都会通过 enqueueUpdate
方法插入到 updateQueue
队列上
当插入u3
后:
fiber.updateQueue.shared.pending === u3;
u3.next === u3;
shared.pending
的环状链表,用图表示为:
fiber.updateQueue.shared.pending: u3 ─────┐
^ |
└──────┘
接着插入u4
之后:
fiber.updateQueue.shared.pending === u4;
u4.next === u3;
u3.next === u4;
shared.pending
是环状链表,用图表示为:
fiber.updateQueue.shared.pending: u4 ──> u3
^ |
└──────┘
shared.pending
会保证始终指向最后一个插入的update
,你可以在这里 (opens new window)看到enqueueUpdate
的源码
更新调度完成后进入render阶段
。
此时shared.pending
的环被剪开并连接在updateQueue.lastBaseUpdate
后面:
fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4
接下来遍历updateQueue.baseUpdate
链表,以fiber.updateQueue.baseState
为初始state
,依次与遍历到的每个Update
计算并产生新的state
(该操作类比Array.prototype.reduce
)。
在遍历时如果有优先级低的Update
会被跳过。
当遍历完成后获得的state
,就是该Fiber节点
在本次更新的state
(源码中叫做memoizedState
)。
render阶段
的Update操作
由processUpdateQueue
完成,你可以从这里 (opens new window)看到processUpdateQueue
的源码
state
的变化在render阶段
产生与上次更新不同的JSX
对象,通过Diff算法
产生effectTag
,在commit阶段
渲染在页面上。
渲染完成后workInProgress Fiber树
变为current Fiber树
,整个更新流程结束。
下面完整的走通ReactDOM.render
完成页面渲染的整个流程。
创建fiber
首次执行ReactDOM.render
会创建fiberRootNode
和rootFiber
。其中fiberRootNode
是整个应用的根节点,rootFiber
是要渲染组件所在组件树的根节点
。
这一步发生在调用ReactDOM.render
后进入的legacyRenderSubtreeIntoContainer
方法中。
// container指ReactDOM.render的第二个参数(即应用挂载的DOM节点)
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
你可以从这里 (opens new window)看到这一步的代码
legacyCreateRootFromDOMContainer
方法内部会调用createFiberRoot
方法完成fiberRootNode
和rootFiber
的创建以及关联。并初始化updateQueue
。
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRootNode
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 连接rootFiber与fiberRootNode
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 初始化updateQueue
initializeUpdateQueue(uninitializedFiber);
return root;
}
补充上rootFiber
到fiberRootNode
的引用。
你可以从这里 (opens new window)看到这一步的代码
创建update
我们已经做好了组件的初始化工作,接下来就等待创建Update
来开启一次更新。
这一步发生在updateContainer
方法中。
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...省略与逻辑不相关代码
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
// update.payload为需要挂载在根节点的组件
update.payload = {element};
// callback为ReactDOM.render的第三个参数 —— 回调函数
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将生成的update加入updateQueue
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);
// ...省略与逻辑不相关代码
}
你可以从这里 (opens new window)看到
updateContainer
的代码
值得注意的是其中update.payload = {element};
对于HostRoot
,payload
为ReactDOM.render
的第一个传参。
流程概览
至此,ReactDOM.render
的流程就和我们已知的流程连接上了。
整个流程如下:
创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)
|
|
v
创建Update对象(`updateContainer`)
|
|
v
从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
|
v
调度更新(`ensureRootIsScheduled`)
|
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
commit阶段(`commitRoot`)
this.setState
当我们有了前面知识的铺垫,就很容易理解this.setState
的工作流程。
可以看到,this.setState
内会调用this.updater.enqueueSetState
方法。
Component.prototype.setState = function (partialState, callback) {
if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
{
throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
}
}
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
你可以在这里 (opens new window)看到这段代码
在enqueueSetState
方法中就是我们熟悉的从创建update
到调度update
的流程了。
enqueueSetState(inst, payload, callback) {
// 通过组件实例获取对应fiber
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// 获取优先级
const lane = requestUpdateLane(fiber, suspenseConfig);
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
// 赋值回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 将update插入updateQueue
enqueueUpdate(fiber, update);
// 调度update
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
你可以在这里 (opens new window)看到
enqueueSetState
代码
这里值得注意的是对于ClassComponent
,update.payload
为this.setState
的第一个传参(即要改变的state
)。
this.forceUpdate
在this.updater
上,除了enqueueSetState
外,还存在enqueueForceUpdate
,当我们调用this.forceUpdate
时会调用他。
可以看到,除了赋值update.tag = ForceUpdate;
以及没有payload
外,其他逻辑与this.setState
一致。
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
// 赋值tag为ForceUpdate
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane, eventTime);
},
};
你可以在这里 (opens new window)看到
enqueueForceUpdate
代码
那么赋值update.tag = ForceUpdate;
有何作用呢?
在判断ClassComponent
是否需要更新时有两个条件需要满足:
const shouldUpdate =
checkHasForceUpdateAfterProcessing() ||
checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
);
你可以在这里 (opens new window)看到这段代码
- checkHasForceUpdateAfterProcessing:内部会判断本次更新的
Update
是否为ForceUpdate
。即如果本次更新的Update
中存在tag
为ForceUpdate
,则返回true
。 - checkShouldComponentUpdate:内部会调用
shouldComponentUpdate
方法。以及当该ClassComponent
为PureComponent
时会浅比较state
与props
。
你可以在这里 (opens new window)看到
checkShouldComponentUpdate
代码
所以,当某次更新含有tag
为ForceUpdate
的Update
,那么当前ClassComponent
不会受其他性能优化手段
(shouldComponentUpdate
|PureComponent
)影响,一定会更新。
cessing:内部会判断本次更新的Update
是否为ForceUpdate
。即如果本次更新的Update
中存在tag
为ForceUpdate
,则返回true
。
- checkShouldComponentUpdate:内部会调用
shouldComponentUpdate
方法。以及当该ClassComponent
为PureComponent
时会浅比较state
与props
。
你可以在这里 (opens new window)看到
checkShouldComponentUpdate
代码
所以,当某次更新含有tag
为ForceUpdate
的Update
,那么当前ClassComponent
不会受其他性能优化手段
(shouldComponentUpdate
|PureComponent
)影响,一定会更新。
至此,我们学习完了HostRoot | ClassComponent
所使用的Update
的更新流程。