用过 react 的小伙伴一定知道 setState 这个API,今天我们来看下 setState 用在不同场景中有什么不同。
注意,这里基于react 16.13.0
合成事件中的setState
首先得了解一下什么是合成事件,react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
class App extends Component {
state = { count: 0 }
increase = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出的是更新前的count --> 0
}
render() {
return (
<div onClick={this.increase}>
count: {this.state.count}
</div>
)
}
}
生命周期函数中的setState
class App extends Component {
state = { count: 0 }
componentDidMount() {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出的是更新前的count --> 0
}
render() {
return (
<div onClick={this.increase}>
count: {this.state.count}
</div>
)
}
}
在原生事件上的setState
class App extends Component {
state = { count: 0 }
componentDidMount() {
document.body.addEventListener('click', this.changeCount, false)
}
changeCount = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新后的count --> 1
}
render() {
return (
<div>
count: {this.state.count}
</div>
)
}
}
setTimeout 中的setState
class App extends Component {
state = { count: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出的是更新后的count --> 1
}, 0)
}
render() {
return (
<div>
count: {this.state.count}
</div>
)
}
}
批量 setState 更新
class App extends Component {
state = { count: 0 }
increase = () => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
console.log(this.state.count) // 输出的是更新前的count --> 0
}
render() {
return (
<div onClick={this.increase}>
count: {this.state.count} // 0
</div>
)
}
}
综上,出个题目考考大家。如果大家读了上面的内容应该很快能够给出答案。
class App extends React.Component {
state = { count: 0 }
componentDidMount() {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count);
this.setState({ count: this.state.count + 1 })
console.log(this.state.count);
setTimeout(_ => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count);
this.setState({ count: this.state.count + 1 })
console.log(this.state.count);
}, 0)
}
render() {
return <div>{this.state.count}</div> // 3
}
}
小结
-
setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
-
setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
-
setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
那 setState 在不同场景下为什么会有不一样的表现呢?接下来我们来看下原因
通过源码解读:
首先 Component 定义props、context、refs,之后注入一个更新器。
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject; // 如果一个组件有字符串引用,我们稍后将分配一个不同的对象。
this.updater = updater || ReactNoopUpdateQueue; // 初始化更新器,实际没有则使用默认更新器
}
我们可以看到 setState 是在组件的原型上定义的,它有两个参数:partialState 和 callback。首先它会检测 partialState 的类型,partialState 是一个object、function或者null,如果不是这三个类型就会抛错。然后执行更新器的setState队列。执行 enqueueSetState。
Component.prototype.setState = function (partialState, callback) {
if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
{ ... // 提示 }
}
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
在更新器上的检测操作,检测如果在组件没有 mounted,就调用 setState 会报警告。
var ReactNoopUpdateQueue = {
...
enqueueSetState: function (publicInstance, partialState, callback, callerName) {
warnNoop(publicInstance, 'setState');
}
};
function warnNoop(publicInstance, callerName) {
{
var _constructor = publicInstance.constructor;
var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';
var warningKey = componentName + "." + callerName;
if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
return;
}
error("错误提示", callerName, componentName);
didWarnStateUpdateForUnmountedComponent[warningKey] = true;
}
}
接下来,在 DOM 加载的时候把 updater 注入进来。
var classComponentUpdater = {
isMounted: isMounted, // 检测组件是否已经 mounted。
// setState队列
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var currentTime = requestCurrentTimeForUpdate();
var suspenseConfig = requestCurrentSuspenseConfig();
var expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);
var update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
{ warnOnInvalidCallback(callback, 'setState') }
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
...
};
我们直接来看 setState 队列,这里需要3个参数分别是实例对象,负载和回调函数。在这里我们先看在最开始声明5个变量分别是干什么用的。
- fiber:通过对象记录组件上需要做或者已经完成的更新,一个组件可以对应多个Fiber。简单点来说,就是通过对象的形式描述一个DOM
function get(key) {
return key._reactInternalFiber;
}
function has(key) {
return key._reactInternalFiber !== undefined;
}
function set(key, value) {
key._reactInternalFiber = value;
}
在render函数中创建的React Element树在第一次渲染的时候会创建一颗结构一模一样的Fiber节点树。不同的React Element类型对应不同的Fiber节点类型。一个React Element的工作就由它对应的Fiber节点来负责。
一个React Element可以对应不止一个Fiber,因为Fiber在更新的时候,会从原来的Fiber(current)克隆出一个新的Fiber(alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。
- currentTime:获取当前更新时间,是否批量跟新事件的不同时间处理
function requestCurrentTimeForUpdate() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// 我们在React内部,因此可以读取实际时间。设置同步更新的时间
return msToExpirationTime(now());
}
// 可能处于浏览器事件的中间。这里是设置所谓的‘异步’更新的时间
if (currentEventTime !== NoWork) {
// 对所有更新使用相同的开始时间,直到我们再次输入React。
return currentEventTime;
}
// 这是自React产生以来的第一次更新。 计算新的开始时间。
currentEventTime = msToExpirationTime(now());
return currentEventTime;
}
- suspenseConfig: 一些配置,不过多解释,有需要可以自己去查阅相关文档
我们一步步找上去,suspenseConfig 的逻辑如下:
var Internals = {
Events: [
getInstanceFromNode$1,
getNodeFromInstance$1,
getFiberCurrentPropsFromNode$1,
injectEventPluginsByName,
eventNameDispatchConfigs,
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
enqueueStateRestore,
restoreStateIfNeeded,
dispatchEvent, // 这里有处理 setState 更新机制的逻辑,在文章最后介绍
runEventsInBatch,
flushPassiveEffects,
IsThisRendererActing
]
};
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = Internals
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
var ReactCurrentBatchConfig = ReactSharedInternals.ReactCurrentBatchConfig;
function requestCurrentSuspenseConfig() {
return ReactCurrentBatchConfig.suspense;
}
- expirationTime
// 计算优先等级函数
function getCurrentPriorityLevel() {
switch (Scheduler_getCurrentPriorityLevel()) {
case Scheduler_ImmediatePriority:
return ImmediatePriority; // 99
case Scheduler_UserBlockingPriority:
return UserBlockingPriority$1; // 98
case Scheduler_NormalPriority:
return NormalPriority; // 97
case Scheduler_LowPriority:
return LowPriority; // 96
case Scheduler_IdlePriority:
return IdlePriority; // 95
default:
{
{
throw Error( "Unknown priority level." );
}
}
}
}
function computeExpirationForFiber(currentTime, fiber, suspenseConfig) {
var mode = fiber.mode;
// 同步模式,变量 noMode = 0
if ((mode & BlockingMode) === NoMode) {
return Sync;
}
var priorityLevel = getCurrentPriorityLevel(); // 计算优先等级
// 并发模式
if ((mode & ConcurrentMode) === NoMode) {
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
// 同步跟新的情况
if ((executionContext & RenderContext) !== NoContext) {
// 在 React 内部,同步更新
return renderExpirationTime$1;
}
var expirationTime;
if (suspenseConfig !== null) {
// 根据挂起超时计算到期时间。
// 如果timeoutMs小于正常pri到期时间,我们是否应该发出警告?
expirationTime = computeSuspenseExpiration(currentTime, suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION);
} else {
// 根据调度程序优先级计算到期时间。
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority$1:
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority:
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Idle;
break;
default:
{
{
throw Error( "Expected a valid priority level" );
}
}
}
}
if (workInProgressRoot !== null && expirationTime === renderExpirationTime$1) {
// 这是将此更新移到单独批处理中的技巧
expirationTime -= 1;
}
return expirationTime;
}
- update:更新器
function createUpdate(expirationTime, suspenseConfig) {
var update = {
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null
};
update.next = update;
{
update.priority = getCurrentPriorityLevel(); // 获取当前优先级
}
return update;
}
在定义了这五个变量之后,检测 callback 是否有效,有效则加入 update。接下来执行下面两个函数
- enqueueUpdate(fiber, 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;
{
if (currentlyProcessingQueue === sharedQueue && !didWarnUpdateInsideUpdate) {
... // 提示
didWarnUpdateInsideUpdate = true;
}
}
}
- scheduleWork(fiber, expirationTime):执行更新。重点,这里就是 setState 的同步和‘异步’区分了
var scheduleWork = scheduleUpdateOnFiber; // 这被拆分为一个单独的函数,因此我们可以将 fiber 标记为待处理的工作(react的合成事件等),而无需将其视为源自事件的典型更新。
function scheduleUpdateOnFiber(fiber, expirationTime) {
checkForNestedUpdates(); // React限制了嵌套更新的数量和最大更新深度。
warnAboutRenderPhaseUpdatesInDEV(fiber); // DEV 中的警告
var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); // 标记从 fiber 到根的更新时间
if (root === null) {
// 内存泄漏的警告
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
checkForInterruption(fiber, expirationTime); // 检查是否中断
recordScheduleUpdate(); // 记录时间表更新
var priorityLevel = getCurrentPriorityLevel(); // 获取当前优先级
if (expirationTime === Sync) {
if ( // 检查我们是否在批量更新内
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext) { // 检查我们是否尚未渲染
// 在根上注册待处理的交互,以避免丢失跟踪的交互数据。主要是更新记录异步函数的数量
schedulePendingInteractions(root, expirationTime);
// 根执行同步工作
// 这是一个遗留的边缘情况。 在batchedUpdates内部的ReactDOM.render根的初始安装应该是同步的,但是布局更新应该推迟到批处理结束。
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root); // 确保根已安排更新处理
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// 立即清除同步工作,除非我们已经在工作或在批量处理中。故意将其放置在scheduleUpdateOnFiber而不是scheduleCallbackForFiber内,以保留在不立即刷新回调的情况下调度回调的功能。我们仅对用户启动的更新执行此操作,以保留旧版模式的历史行为。
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
if ((executionContext & DiscreteEventContext) !== NoContext && (
// 即使具有离散事件,也仅将具有用户阻塞优先级或更高优先级的更新视为离散。
priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
// 这是离散事件的结果。跟踪每个根目录的最低优先级离散更新,以便我们可以在需要时及早刷新它们。
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
这里是处理事件的机制:
function dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
if (!_enabled) {
return;
}
// 如果我们已经有一个离散事件队列,并且这是另一个离散事件,则无论目标是什么,我们都无法分派它,因为它们需要按顺序分派。也就是说如果有排队的离散事件或是可重播的离散事件
if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
queueDiscreteEvent(null, topLevelType, eventSystemFlags, container, nativeEvent);
// 据我们所知,实际上我们并未对任何内容进行阻止的标志。
return;
}
// 尝试调度事件看有无原生事件
var blockedOn = attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent);
if (blockedOn === null) {
// 我们成功调度了此事件。
clearIfContinuousEvent(topLevelType, nativeEvent);
return;
}
// 如果是可重播的离散事件
if (isReplayableDiscreteEvent(topLevelType)) {
// 目标可用时,将在以后重播此内容。
queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, container, nativeEvent);
return;
}
// 如果是连续事件则排队
if (queueIfContinuousEvent(blockedOn, topLevelType, eventSystemFlags, container, nativeEvent)) {
return;
}
// 我们只需要在不排队的情况下进行清除,因为排队是累积性的。
clearIfContinuousEvent(topLevelType, nativeEvent);
// 这是不可重播的,因此在事件系统需要跟踪它的情况下,我们将调用它,但没有目标
{
dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, null);
}
}
function attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
// 原生事件
var nativeEventTarget = getEventTarget(nativeEvent);
// 从节点获取最近的实例
var targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
// 获得最近安装的 fiber
var nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// 该树已被卸载。 没有目标就派遣。
targetInst = null;
} else {
var tag = nearestMounted.tag;
if (tag === SuspenseComponent) { // SuspenseComponent = 13
// 从 Fiber 获取挂起实例
var instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// 将事件排队以便稍后重播。 中止调度,因为我们不希望此事件通过事件系统调度两次。如果这是队列中的第一个离散事件。 为该边界安排更高的优先级。
return instance;
}
// 这不应该发生,出了点问题,但是为了避免阻塞整个系统,请在没有目标的情况下调度事件。
targetInst = null;
} else if (tag === HostRoot) { // HostRoot = 3
var root = nearestMounted.stateNode;
if (root.hydrate) {
// 如果在重播期间发生这种情况,则可能出了点问题,可能会阻塞整个系统
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
// 如果在提交该组件的安装之前收到一个事件(例如:img onload),请暂时将其忽略(即,将其视为非响应树上的事件)。 我们也可以考虑对事件进行排队,然后在挂载后调度它们。
targetInst = null;
}
}
}
{
dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst);
}
// 我们没有任何阻碍。
return null;
}
为旧版事件系统调度事件
function dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst, eventSystemFlags);
try {
// 在合成事件前调用 batchedEventUpdates,以设置 isBatchingEventUpdates 为 true or false
batchedEventUpdates(
handleTopLevel, // 这里的逻辑应该不难理解,在调用任何事件处理程序之前,先构建祖先数组
bookKeeping
);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping); // 初始化为调用 setState 事件前的状态
}
}
这里是 setState 同步或者‘异步’的表现:如果 isBatchingEventUpdates 为 true,则先执行 batchedEventUpdatesImpl 这个函数,最后执行 setState 的事件
function batchedEventUpdates(fn, a, b) {
if (isBatchingEventUpdates) {
// 如果我们当前在另一个批次中,则需要等到它完全完成后再恢复状态。
return fn(a, b);
}
isBatchingEventUpdates = true; // 决定 setState 为同步或者 ‘异步’的标识
try {
return batchedEventUpdatesImpl(fn, a, b);
} finally {
isBatchingEventUpdates = false;
finishEventHandler();
}
}
以下逻辑就不再继续介绍了,有需要的小伙伴可以自行查阅文档
function finishEventHandler() {
// 在这里,我们等待所有更新均已传播,这在使用层中的受控组件时很重要,然后,我们恢复任何受控组件的状态。
var controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
// 如果触发了受控事件,则可能需要将DOM节点的状态恢复回受控值。 当React退出更新而不接触DOM时,这是必需的。
flushDiscreteUpdatesImpl();
restoreStateIfNeeded();
}
}
好了,今天就说到这里吧,有不对的地方请各位小伙伴指出哦。一起成长。
总结
setState 在原生事件或者 setTimeout 和 setInterval 事件中表现为同步。
setState 在合成事件和生命周期中表现为异步: 首先在合成事件和生命周期前会执行一个名为 batchedEventUpdates 的函数来设置 isBatchingEventUpdates 的值为 false 还是 true。如果为 false,则直接修改 state 表现为同步,如果值为 true,则先执行setState事件,但是没有更新状态,执行完了之后才去更新状态,所以在这里表现为‘异步’。