点击这里进入react原理专栏
这篇文章会分别讲解一个类组件是如何创建和更新的,也就是类组件的初次挂载和后续更新的流程。
首先看更新类组件的入口:updateClassComponent
function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
// 。。。
if (instance === null) {
// 挂载
if (current !== null) {
current.alternate = null;
workInProgress.alternate = null;
workInProgress.flags |= Placement;
}
// 创建类实例
constructClassInstance(workInProgress, Component, nextProps);
// 挂载类组件
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
} else {
// 更新
shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
}
// diff
var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
// 。。。
return nextUnitOfWork;
}
上面的注释已经把各个函数的调用阶段说的比较明白了,接下来先看挂载阶段的流程
finishClassComponent
方法涉及到diff
的流程,内容比较多,因此本篇暂时不讲。
挂载阶段
首先是创建类实例:constructClassInstance
function constructClassInstance(workInProgress, ctor, props) {
// ...
var instance = new ctor(props, context);
var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
adoptClassInstance(workInProgress, instance);
// ...
}
最主要的就是这三行代码,首先执行类的constructor
方法,之后确定state
,这里,fiber
的memoizedState
就是本次更新结束之后,组件的state
。由于时初次挂载,直接使用constructor
中定义的this.state
即可,接下来是adoptClassInstance
function adoptClassInstance(workInProgress, instance) {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// 指定一个内部属性
set(instance, workInProgress);
}
这里为类实例添加了一个updater
属性,看一下setState
的源码
Component.prototype.setState = function (partialState, callback) {
// ...
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
再看一下classComponentUpdater
是什么
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
// ...
},
enqueueReplaceState: function (inst, payload, callback) {
// ...
},
enqueueForceUpdate: function (inst, callback) {
// ...
}
};
相信大家已经明白了。
类实例创建完成后,看一下挂载组件的mountClassInstance
方法
function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
// ...
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
var getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
instance.state = workInProgress.memoizedState;
}
if (typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function')) {
callComponentWillMount(workInProgress, instance);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
// ...
}
这个方法主要做了两件事:计算state
(processUpdateQueue
)和生命周期方法的调用。关于state
的计算,会在后面讲解。从生命周期部分的代码可以看到,react
会先调用getDerivedStateFromProps
钩子,之后如果没有新的生命周期(getDerivedStateFromProps
和getSnapshotBeforeUpdate
),调用UNSAFE_componentWillMount
。由于UNSAFE_componentWillMount
中可能会再次更新state
,所以会再次调用processUpdateQueue
。
后续更新
类组件更新的入口函数为updateClassInstance
。但是在讲解该方法之前,先看一下类组件时如何产生更新的,也就是setState
的流程。
前面已经看过了,setState
会调用classComponentUpdater
的enqueueSetState
,定义如下
enqueueReplaceState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
// 创建更新
var update = createUpdate(eventTime, lane);
update.tag = ReplaceState;
// payload就是setState的第一个参数
update.payload = payload;
if (callback !== undefined && callback !== null) {
{
warnOnInvalidCallback(callback, 'replaceState');
}
// callback就是setState的回调
update.callback = callback;
}
// 将update对象放入队列
enqueueUpdate(fiber, update);
// 调度更新
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
首先看一下fiber
的一个属性:fiber.updateQueue
,updateQueue
中存放着fiber
中的所有更新,updateQueue
有一个shared
属性,该属性是一个环形链表,shared
有一个pending
属性,该属性指向环形链表的最后一个节点,就是最新的一个update
对象。知道了这些,enqueueUpdate
的源码就能看懂了
// enqueueUpdate就是讲update放入环形链表中,并让pending执行最新的一个update
function enqueueUpdate(fiber, update) {
var updateQueue = fiber.updateQueue;
if (updateQueue === null) {
return;
}
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
之后,我们又来到了scheduleUpdateOnFiber
方法。看这段代码
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext) {
schedulePendingInteractions(root, lane);
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
在ReactDom.render
这篇文章中提到过,只有在ReactDom.render
才会进入if的逻辑,之后的更新会进入else的逻辑,因此,会执行ensureRootIsScheduled
。
ensureRootIsScheduled
中会调度执行performSyncWorkOnRoot
function ensureRootIsScheduled(root, currentTime) {
// ...
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// ...
}
}
回想一下ReactDOM.render
是直接执行performSyncWorkOnRoot
,而setState
则是调度执行performSyncWorkOnRoot
。scheduleSyncCallback
代码如下:
function scheduleSyncCallback(callback) {
if (syncQueue === null) {
syncQueue = [callback];
immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
} else {
syncQueue.push(callback);
}
return fakeCallbackNode;
}
Scheduler_scheduleCallback
是scheduler
模块暴露出的scheduleCallback
,就是会在浏览器空闲时间调度某个函数。而scheduleSyncCallback
就是将performSyncWorkOnRoot
放入syncQueue
中,之后调度flushSyncCallbackQueueImpl
。但是在我们目前使用的legacy
模式中,情况并不是这样的。
setState为什么是异步的
其实在类组件中,我们使用setState
一般有两种情况,在生命周期中使用(componentDidMount
),在点击事件中使用。在讲解ReactDOM.render
和合成事件的文章中,我们忽略了两个方法:unbatchedUpdates
和batchedEventUpdates$1
function unbatchedUpdates(fn, a) {
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
可以发现,这两个函数很像,都是执行回调函数fn
之后,如果当前没有正在执行的任务(executionContext === NoContext
),执行flushSyncCallbackQueue
来清空回调队列(syncQueue
)。在unbatchedUpdates
中,fn
就是updateContainer
,在batchedEventUpdates$1
中,fn
就是dispatchEventsForPlugins
。再结合ensureRootIsScheduled
中调用的scheduleSyncCallback
,就能直到为什么setState
是异步的了。因为在react
中,会将setState
触发的更新函数performSyncWorkOnRoot
放入一个队列中(syncQueue
),等同步代码执行完毕后,再来执行回调队列中的任务。
现在大家可能又疑问了,scheduleSyncCallback
不是也调度了一个任务吗,难道说performSyncWorkOnRoot
会执行两次吗。当然不会,看一下flushSyncCallbackQueue
的代码
function flushSyncCallbackQueue() {
if (immediateQueueCallbackNode !== null) {
var node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
flushSyncCallbackQueueImpl();
}
Scheduler_scheduleCallback
调度任务一个任务对象,赋值给immediateQueueCallbackNode
,在执行flushSyncCallbackQueue
时又取消了该任务(flushSyncCallbackQueueImpl
),之后再执行flushSyncCallbackQueueImpl
,开始执行syncQueen
中的任务。至于为什么要这么做,个人认为,react17
作为一个过渡版本,为了渐进升级而做的一种兼容。
因为
scheduler
中使用宏任务来调度任务,而unbatchedUpdates
和batchedEventUpdates$1
中的代码是同步代码,因此会先于scheduler
执行
至于flushSyncCallbackQueueImpl
的内容就比较简单了,就是循环syncQueue
中的任务
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
// 一个开关,防止重复循环任务队列
isFlushingSyncQueue = true;
var i = 0;
{
try {
var _isSync2 = true;
var _queue = syncQueue;
runWithPriority$1(ImmediatePriority$1, function () {
for (; i < _queue.length; i++) {
var callback = _queue[i];
do {
callback = callback(_isSync2);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// ...
}
}
}
}
setState有时还是同步的?
如果我们在setState
外面加上了一层setTimeout
,setState
就又变成了同步执行,这又是为什么呢?其实,setState
异步执行不仅仅因为react
将performSyncWorkOnRoot
放入了一个队列中,还有一个重要原因就是执行时机。注意scheduleUpdateOnFiber
中有这样一段代码
if (
// ...
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
setState
会执行ensureRootIsScheduled
,将performSyncWorkOnRoot
放入队列中,之后有一个判断,如果executionContext === NoContext
,就执行flushSyncCallbackQueue
。由于我们的setState
是在setTimeout
中的,当执行setTimeout
的回调函数时,是获取不到react
内部的执行上下文的,因此executionContext === NoContext
成立,执行flushSyncCallbackQueue
,之后就会取消ensureRootIsScheduled
调度的任务,执行任务队列中的performSyncWorkOnRoot
,这就是setState
同步执行的原因。
针对这个问题,react
提供了一个api:unstable_batchedUpdates
,代码也很简单,就是额外提供了一个执行上下文:
function batchedUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
// 这样使用即可
unstable_batchedUpdates(
setTimeout(() => {
setState({
// ...
})
})
)
setState批处理的原理
此外,react
还提供了批处理的功能。其实,前面关于setState
异步原因的讨论中,已经涉及到批处理的内容了,这里主要讲一下,当我们使用多个setState
时,react
是如何保证只触发一次更新的,重点在于ensureRootIsScheduled
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackNode = root.callbackNode;
// ...
if (existingCallbackNode !== null) {
var existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// 后续setState直接返回
return;
}
cancelCallback(existingCallbackNode);
}
// ...
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// ...
}
// ...
root.callbackNode = newCallbackNode;
}
当第一次进入ensureRootIsScheduled
,root.callbackNode
为空,之后经过scheduleSyncCallback
,root.callbackNode
被赋值为newCallbackNode
。之后的setState
还会进入ensureRootIsScheduled
,而此时existingCallbackNode
不为空,并且多个setState
的优先级相同,因此会直接返回。也就是说,只有第一次setState
会将更新入口函数performSyncWorkOnRoot
放入队列中,后续setState
只会将update
对象放入fiber.updateQueue
中,不会触发更新入口函数。
如果在
setTimeout
中使用多次setState
,因为没有了执行上下文,在scheduleUpdateOnFiber
中,ensureRootIsScheduled
调度任务后,就会执行flushSyncCallbackQueue
了
类组件的更新入口
讲了这么多,都在说setState
的原理,现在回到类组件的更新部分,入口函数为updateClassInstance
。这部分内容比较简单,首先就是执行componentWillReceiveProps
生命周期(如果没有新的生命周期钩子的话),之后计算更新。
state的计算原理
终于到了这篇文章的重头戏:state
是如何计算的。入口函数为processUpdateQueue
。这部分代码很长,所以我们先明白整体流程,再看源码。
processUpdateQueue
方法是兼容concurrent mode
的,所以我们考虑这个例子:
A1 -> B2 -> C1 -> D1
字母代表state
的变化,数字代表优先级,数字越小,优先级越高。
因此在concurrent mode
下,B2
由于优先级较低,会被跳过,因此第一次更新计算过程中,结果为ACD,下一轮更新计算时,就计算B了。并且我们要保证state
的最终结果是正确的,也就是ABCD,不能是ACDB。因此,react
在处理更新时,使用了第二条链表:baseUpdate
链表以及另一个属性:baseState
。下面来梳理一下整个流程
- 首先,因为
updateQueue
是环形链表,因此要将它剪断,成为一个单向链表 - 遍历
updateQueue
,来到A1,因为优先级足够,因此对A1进行状态计算 - 来到B2,由于优先级不够,将B2放到
baseUpdate
链表中,并且B2之后的所有节点都放入baseUpdate
中,将A1作为baseState
,baseUpdate
变为B2 -> C1 -> D1
- 之后的C1和D1的优先级都足够,进行计算
- 此时页面显示ACD
- 第二轮更新开始,此时
updateQueue
中没有内容(updateQueue
存放的是用户产生的新更新,不是上次遗留的更新。如果updateQueue
中有更新的话,将updateQueue
剪断,连接到baseUpdate
后面) - 遍历
baseUpdate
链表,以baseState
作为基准,开始计算,也就是说,从B2开始,以A1为基准,计算state
。此时的baseUpdate
为B2 -> C1 -> D1
- 计算完毕,页面显示ABCD
因此状态更新计算的关键内容在于,baseUpdate
保存因为优先级不足而被跳过的节点(包括该节点后面的所有节点),baseState
保存被跳过节点的前一个节点计算之后的state
,下一轮的更新就会基于这个baseState
,将baseUpdate
再循环一遍。
源码
function processUpdateQueue(workInProgress, props, instance, renderLanes) {
var queue = workInProgress.updateQueue;
hasForceUpdate = false;
{
currentlyProcessingQueue = queue.shared;
}
// react使用firstBaseUpdate表示baseUpdate链表的第一个节点,lastBaseUpdate表示最后一个节点
var firstBaseUpdate = queue.firstBaseUpdate;
var lastBaseUpdate = queue.lastBaseUpdate;
var pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// 剪断环形链表
queue.shared.pending = null;
var lastPendingUpdate = pendingQueue;
var firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
// 将updateQueue拼接到baseUpdate后面
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
var current = workInProgress.alternate;
// 对current做同样的操作,是为了保证即便本次更新被打断,之后根据current创建workInProgress时,能够得到正确的更新队列
if (current !== null) {
var currentQueue = current.updateQueue;
var currentLastBaseUpdate = currentQueue.lastBaseUpdate;
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}
if (firstBaseUpdate !== null) {
var newState = queue.baseState;
var newLanes = NoLanes;
var newBaseState = null;
var newFirstBaseUpdate = null;
var newLastBaseUpdate = null;
var update = firstBaseUpdate;
do {
var updateLane = update.lane;
var updateEventTime = update.eventTime;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 该节点优先级不足
var clone = {
eventTime: updateEventTime,
lane: updateLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null
};
// 把被跳过的节点放入baseUpdate链表中
if (newLastBaseUpdate === null) {
newFirstBaseUpdate = newLastBaseUpdate = clone;
newBaseState = newState;
} else {
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
newLanes = mergeLanes(newLanes, updateLane);
} else {
// 该节点优先级足够
if (newLastBaseUpdate !== null) {
// 如果之前有过优先级不足的节点,则该优先级不足节点之后的每个节点,都要被放入baseUpdate链表中
var _clone = {
eventTime: updateEventTime,
lane: NoLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null
};
newLastBaseUpdate = newLastBaseUpdate.next = _clone;
}
// 计算新的state
newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
var callback = update.callback;
// setState的回调函数,放入effects数组中
if (callback !== null) {
workInProgress.flags |= Callback;
var effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
}
// 循环链表
update = update.next;
if (update === null) {
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
break;
} else {
// 如果有了新的update产生,放入updateQueue
var _lastPendingUpdate = pendingQueue;
var _firstPendingUpdate = _lastPendingUpdate.next;
_lastPendingUpdate.next = null;
update = _firstPendingUpdate;
queue.lastBaseUpdate = _lastPendingUpdate;
queue.shared.pending = null;
}
}
} while (true);
if (newLastBaseUpdate === null) {
newBaseState = newState;
}
queue.baseState = newBaseState;
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
// memoizedState就是本次状态更新计算完成之后的state
workInProgress.memoizedState = newState;
}
{
currentlyProcessingQueue = null;
}
}
在计算state时,使用Object.assign进行合并
总结
本文讲解了setState
的流程,类组件的状态更新原理,以及setState
异步的原因,希望能够帮到大家。