问题
又一次的bug,不多说了,都是泪,这里直接贴一下“有问题的代码”吧
import React from "react";
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: 1,
flag: false,
};
}
onValidateSuccessSubmit = (data) => {
this.setState({ flag: true });
// do something
this.setState({ data });
};
asyncFunc = () => {
return true;
};
onClick = async () => {
const validate = await this.asyncFunc();
if (validate) {
this.onValidateSuccessSubmit(2);
} else {
// do something
}
};
onSubmit = () => {
console.log(this.state.data);
};
render() {
return (
<div onClick={this.onClick}>
<div>click it</div>
<ChildComponent flag={this.state.flag} onSubmit={this.onSubmit} />
</div>
);
}
}
export default Parent;
class ChildComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.flag) {
nextProps.onSubmit();
}
}
render() {
return <div>child component</div>;
}
}
复制代码
简单说一下这段代码的逻辑:
- 点击parent组件执行onClick事件,此事件会通过async/await拿到一个变量。
- 执行onValidateSuccessSubmit事件,这个事件触发两次setState
- setState把flag置为true,触发子组件的componentWillReceiveProps钩子函数,执行父组件的onSubmit函数。
- 父组件的onSubmit函数输出state中的数据。
这时的console会输出两次,结果分别是1和2,所以当我们在使用onSubmit函数处理业务逻辑时,拿到的也是更新之前的state,然后就没有然后了?
大家可以先思考一下为什么是这样?是什么原因导致的呢?
初步猜想
因为输出了两次,并且我们也都知道setState有同步和异步,所以会不会是setState的这种同步异步状态导致的呢。为了验证我们的猜想,我们把其中的关键代码改成这样:
onClick = () => {
const validate = this.asyncFunc();
if (validate) {
setTimeout(() => {
this.onValidateSuccessSubmit(2);
}, 0);
} else {
// do something
}
};
复制代码
果不其然,输出的结果和使用async/await的一致,再看一下源码,验证一下运行的流程是否完全一致。
合成事件setState源码
下面我们就看一下当我们setState时,react具体是怎么做的(react版本16.12.0)
首先看一下正常的合成事件中setState,此时关键代码如下:
onClick = () => {
const validate = this.asyncFunc();
if (validate) {
this.onValidateSuccessSubmit(2);
} else {
// do something
}
};
复制代码
当我们执行this.setState({ flag: true })
时,react处理流程如下:
免喷声明:由于这是本人通过debugger的同时再基于本人对于react非常浅薄的理解写出来的文章,对于react里非常多的细节处理没有介绍到,还希望大家多多理解,对于其中的错误地方多多指正。
执行setState
// packages/react/src/ReactBaseClasses.js
/**
* Sets a subset of the state. Always use this to mutate
* state. You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* There is no guarantee that calls to `setState` will run synchronously,
* as they may eventually be batched together. You can provide an optional
* callback that will be executed when the call to setState is actually
* completed.
*
* When a function is provided to setState, it will be called at some point in
* the future (not synchronously). It will be called with the up to date
* component arguments (state, props, context). These values can be different
* from this.* because your function may be called after receiveProps but before
* shouldComponentUpdate, and this new state, props, and context will not yet be
* assigned to this.
*
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'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');
};
复制代码
感兴趣的同学可以自行看一下注释,更有助于对react的理解。
setState函数会执行this.updater.enqueueSetState(this, partialState, callback, 'setState');
,其中this就是当前组件了,partialState就是我们将要修改的state,callback就是修改state后的回调,其实也是我们常见的确保state更新之后触发事件的函数。
enqueueSetState
enqueueSetState是挂载在classComponentUpdater上的一个方法,如下所示
// packages/react-reconciler/src/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) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
...
}
复制代码
我们挑重点看一下属性赋值部分
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
复制代码
这个函数会根据当前react的模式返回不同的expirationTime,这里返回的是Sync常量,关于react的legacy、blocking、concurrent三种模式大家可以自行查阅 使用 Concurrent 模式(实验性)- 特性对比
// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
return Sync;
}
...
return expirationTime;
}
复制代码
我们再看一下函数执行部分,enqueueUpdate(fiber, update)
这个函数传入两个参数,fiber即是当前实例对应的fiber,update我们可以看到是通过createUpdate函数创建并返回的一个update对象
// packages/react-reconciler/src/ReactUpdateQueue.js
export function createUpdate(
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
): Update<*> {
let update: Update<*> = {
expirationTime,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
nextEffect: null,
};
if (__DEV__) {
update.priority = getCurrentPriorityLevel();
}
return update;
}
复制代码
enqueueUpdate
这一步的操作主要是给当前的fiber添加updateQueue
// packages/react-reconciler/src/ReactUpdateQueue.js
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternate;
// queue1和queue2是fiber成对出现的队列
// queue1是current queue
// queue2是work-in-progress queue
// 感兴趣的可以看一下此文件上方的注释信息及参考链接中的Fiber架构的工作原理
let queue1;
let queue2;
if (alternate === null) {
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
// 首次执行setState时,fiber的任务队列都为null,执行下面的代码
// createUpdateQueue从函数名我们不难看出此函数用于创建更新队列,参数fiber.memoizedState为constructor中this.state的初始值。
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// There are two owners.
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// Neither fiber has an update queue. Create new ones.
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(
alternate.memoizedState,
);
} else {
// Only one fiber has an update queue. Clone to create a new one.
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// Both owners have an update queue.
}
}
}
if (queue2 === null || queue1 === queue2) {
// There's only a single queue.
// 随后运行下面代码,将需要更新的对象添加至第一个队列中
appendUpdateToQueue(queue1, update);
} else {
// There are two queues. We need to append the update to both queues,
// while accounting for the persistent structure of the list — we don't
// want the same update to be added multiple times.
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// One of the queues is not empty. We must add the update to both queues.
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// Both queues are non-empty. The last update is the same in both lists,
// because of structural sharing. So, only append to one of the lists.
appendUpdateToQueue(queue1, update);
// But we still need to update the `lastUpdate` pointer of queue2.
queue2.lastUpdate = update;
}
}
if (__DEV__) {
if (
fiber.tag === ClassComponent &&
(currentlyProcessingQueue === queue1 ||
(queue2 !== null && currentlyProcessingQueue === queue2)) &&
!didWarnUpdateInsideUpdate
) {
warningWithoutStack(
false,
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
didWarnUpdateInsideUpdate = true;
}
}
}
复制代码
重点是下面两段代码:
...
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
...
appendUpdateToQueue(queue1, update);
...
复制代码
此时updateQueue的firstUpdate和lastUpdate均为createUpdate创建的update对象,此时fiber的updateQueue结构为:
updateQueue: {
baseState: { a: 1, flag: false },
firstCapturedEffect: null,
firstCapturedUpdate: null,
firstEffect: null,
firstUpdate: {
callback: null,
expirationTime: 1073741823,
next: null,
nextEffect: null,
payload: { flag: true },
priority: 98,
suspenseConfig: null,
tag: 0,
},
lastCapturedEffect: null,
lastCapturedUpdate: null,
lastEffect: null,
lastUpdate: {
callback: null,
expirationTime: 1073741823,
next: null,
nextEffect: null,
payload: { flag: true },
priority: 98,
suspenseConfig: null,
tag: 0,
},
},
复制代码
scheduleWork(重点
这里开始进入调度阶段
// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
) {
// 检查是否掉入死循环
checkForNestedUpdates();
// dev环境下的warn,跳过
warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
// 从名字来看是标记fiber到root的更新时间,函数内部主要做了两件事
// fiber.expirationTime置为更大的expirationTime,expirationTime越大优先级越高
// 递归fiber的父节点,并将其childExpirationTime也置为expirationTime
// 不太能理解这个函数,莫非是和react的事件机制有关?
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
checkForInterruption(fiber, expirationTime);
recordScheduleUpdate();
// TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
const priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
if (
// Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// 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.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
...
}
export const scheduleWork = scheduleUpdateOnFiber;
复制代码
直接看重点代码逻辑判断部分,通过上面enqueueSetState的属性赋值我们知道,expirationTime被赋值为Sync常量,所以这里进到
if (
// Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext
)
复制代码
看来这里就是传说中react批处理state的逻辑了,一堆莫名其妙的二进制变量加上位运算符属实让人头大,不过还好这些变量都在该文件内,咱们来慢慢捋一下
const NoContext = /* */ 0b000000;
const BatchedContext = /* */ 0b000001;
const EventContext = /* */ 0b000010;
const DiscreteEventContext = /* */ 0b000100;
const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
...
// Describes where we are in the React execution stack
let executionContext: ExecutionContext = NoContext;
复制代码
此时executionContext
变量值为number6,LegacyUnbatchedContext
值为number0,NoContext
值为number0。executionContext
这个变量大家先着重记一下,表示react执行栈的位置,至于为什么是6,咱们后面再讲。进入判断逻辑,条件(executionContext & LegacyUnbatchedContext) !== NoContext
不符,进入else,
ensureRootIsScheduled
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function ensureRootIsScheduled(root: FiberRoot) {
...
const existingCallbackNode = root.callbackNode;
...
// If there's an existing render task, confirm it has the correct priority and
// expiration time. Otherwise, we'll cancel it and schedule a new one.
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// Callback must have the exact same expiration time.
existingCallbackExpirationTime === expirationTime &&
// Callback must have greater or equal priority.
existingCallbackPriority >= priorityLevel
) {
// Existing callback is sufficient.
return;
}
// Need to schedule a new task.
// TODO: Instead of scheduling a new task, we should be able to change the
// priority of the existing one.
cancelCallback(existingCallbackNode);
}
...
let callbackNode;
if (expirationTime === Sync) {
// Sync React callbacks are scheduled on a special internal queue
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
...
root.callbackNode = callbackNode;
}
复制代码
由于是第一次setState,root中并没有调度任务,进入expirationTime === Sync
逻辑,performSyncWorkOnRoot.bind(null, root)
当成参数传进了scheduleSyncCallback
scheduleSyncCallback
// packages/react-reconciler/src/SchedulerWithReactIntegration.js
const fakeCallbackNode = {};
...
let syncQueue: Array<SchedulerCallback> | null = null;
...
export function scheduleSyncCallback(callback: SchedulerCallback) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
if (syncQueue === null) {
syncQueue = [callback];
// Flush the queue in the next tick, at the earliest.
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
syncQueue.push(callback);
}
return fakeCallbackNode;
}
复制代码
syncQueue是一个数组类型的全局变量,初始值为null,并把performSyncWorkOnRoot.bind(null, root)
给赋值进去,immediateQueueCallbackNode
不影响流程暂不讨论,最后return fakeCallbackNode
,函数内部也没处理fakeCallbackNode,所以返回空对象。返回的这个空对象赋值给了root.callbackNode
。
schedulePendingInteractions
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime);
复制代码
通过注释我们可以了解,这个函数的主要作用是trace,并不影响流程。此任务完成后,进入下个逻辑判断executionContext === NoContext
,条件不符,结束scheduleWork任务。
到这里this.setState({ flag: true })
执行完毕了,我们可以看到,react只是把这个SchedulerCallback
给push进了内部的队列中,并没有diff的操作,也没有触发渲染的逻辑,也正因此setState并不是每次都会触发组件的渲染。
接下来this.setState({ data })
过程由于root已经存在了callbackNode
,所以在ensureRootIsScheduled
中直接return结束任务。
由于篇幅问题,后续的流程及渲染视图过程不多加讨论,感兴趣的同学可以自行研究。
setTimeout时setState源码
关键代码如下:
onClick = () => {
const validate = this.asyncFunc();
if (validate) {
setTimeout(() => {
this.onValidateSuccessSubmit(2);
}, 0);
} else {
// do something
}
};
复制代码
setTimeout时setState过程和合成事件类似,不同之处在于scheduleWork中executionContext的值变成了number 0,所以执行了flushSyncCallbackQueue
,看来合成事件和setTimeout的执行不同之处就在executionContext
和flushSyncCallbackQueue
上面了,我们先来看一下flushSyncCallbackQueue
这个函数做了什么。
flushSyncCallbackQueue
export function flushSyncCallbackQueue() {
if (immediateQueueCallbackNode !== null) {
const node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
flushSyncCallbackQueueImpl();
}
复制代码
flushSyncCallbackQueueImpl
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
// Prevent re-entrancy.
isFlushingSyncQueue = true;
let i = 0;
try {
const isSync = true;
const queue = syncQueue;
runWithPriority(ImmediatePriority, () => {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// Resume flushing in the next tick
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
isFlushingSyncQueue = false;
}
}
}
复制代码
这个方法我们可以清楚的看到try代码块里,拿出了之前的syncQueue任务队列,根据优先级开始执行这些任务。
executionContext
上面setState过程中我们并没有发现该变量有变化,在查阅相关资料后发现是react在处理合成事件时改变了此变量,也就是setState之前对合成事件的处理,我们看一下点击合成事件时的调用栈
从dispatchDiscreteEvent到callCallback都是react在处理合成事件了,经过一番调查,终于给搞清楚了。
discreteUpdates$1
function discreteUpdates$1(fn, a, b, c) {
var prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
try {
// Should this
return runWithPriority$2(UserBlockingPriority$2, fn.bind(null, a, b, c));
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
复制代码
executionContext |= DiscreteEventContext
,关于位操作符不懂得可以自行查阅,这里不再赘述,这里按位或之后赋值给executionContext
,此时executionContext
变量值是0b000100
,也即是十进制中的4。
大家注意一下finally里的代码块,刚进来时prevExecutionContext为0b000000
,try代码块中代码结束后,又把prevExecutionContext赋值给了executionContext
DiscreteEventContext
是全局变量,默认值为0b000100
。而合成事件中onClick就是DiscreteEvent,关于react的事件类型可以参考React 事件 | 1. React 中的事件委托
batchedEventUpdates$1
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
flushSyncCallbackQueue();
}
}
}
复制代码
executionContext |= EventContext
,这里再次按位或之后赋值给executionContext
,此时executionContext
变量值是0b00110
,也即是十进制中的6。
后记
这片文章只是简单叙述一下setState的逻辑,看源码的过程中才发现自己对react知之甚少,知其然不知其所以然,react对合成事件的处理,fiber机制,concurrent模式,渲染视图。。。以后慢慢填坑吧