又一次的bug,很少說了,都是淚,這裏直接貼一下「有問題的代碼」吧javascript
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>;
}
}
複製代碼
簡單說一下這段代碼的邏輯:html
這時的console會輸出兩次,結果分別是1和2,因此當咱們在使用onSubmit函數處理業務邏輯時,拿到的也是更新以前的state,而後就沒有而後了😭java
你們能夠先思考一下爲何是這樣?是什麼緣由致使的呢?node
由於輸出了兩次,而且咱們也都知道setState有同步和異步,因此會不會是setState的這種同步異步狀態致使的呢。爲了驗證咱們的猜測,咱們把其中的關鍵代碼改爲這樣:react
onClick = () => {
const validate = this.asyncFunc();
if (validate) {
setTimeout(() => {
this.onValidateSuccessSubmit(2);
}, 0);
} else {
// do something
}
};
複製代碼
果不其然,輸出的結果和使用async/await的一致,再看一下源碼,驗證一下運行的流程是否徹底一致。segmentfault
下面咱們就看一下當咱們setState時,react具體是怎麼作的(react版本16.12.0)數組
首先看一下正常的合成事件中setState,此時關鍵代碼以下:markdown
onClick = () => {
const validate = this.asyncFunc();
if (validate) {
this.onValidateSuccessSubmit(2);
} else {
// do something
}
};
複製代碼
當咱們執行this.setState({ flag: true })
時,react處理流程以下:架構
免噴聲明:因爲這是本人經過debugger的同時再基於本人對於react很是淺薄的理解寫出來的文章,對於react裏很是多的細節處理沒有介紹到,還但願你們多多理解,對於其中的錯誤地方多多指正。app
// 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是掛載在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;
}
複製代碼
這一步的操做主要是給當前的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,
},
},
複製代碼
這裏開始進入調度階段
// 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,
// 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
// 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
。
// 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結束任務。
因爲篇幅問題,後續的流程及渲染視圖過程很少加討論,感興趣的同窗能夠自行研究。
關鍵代碼以下:
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
這個函數作了什麼。
export function flushSyncCallbackQueue() {
if (immediateQueueCallbackNode !== null) {
const node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
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任務隊列,根據優先級開始執行這些任務。
上面setState過程當中咱們並無發現該變量有變化,在查閱相關資料後發現是react在處理合成事件時改變了此變量,也就是setState以前對合成事件的處理,咱們看一下點擊合成事件時的調用棧
從dispatchDiscreteEvent到callCallback都是react在處理合成事件了,通過一番調查,終於給搞清楚了。
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 中的事件委託
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模式,渲染視圖。。。之後慢慢填坑吧