作者 : 墨成
React 版本 :16.4.1
在React啟示錄裡我們說setState是非同步的,我們在程式碼中也展示了這種特性,那麼FB的工程師們是如何實現呢,本文將基於React的原始碼進一步揭開這層面紗。
在介紹之前我們首先看下setState的實現和FB工程師的註釋,我簡單的作了一些翻譯
//ReactBaseClass.js
/**
* Sets a subset of the state. Always use this to mutate
* state. You should treat `this.state` as immutable.
* //我們應該使用這個方法(setState)來改變state,而不是使用this.state(翻譯可能跟原文有一點偏差,當表達的意思是這樣)
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*//setState並不會立即更新,在呼叫這個方法後拿到的this.state可能還是原來的值
* 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.
*//setState不能保證是同步的,他們有可能會批量處理。你可以提供一個可選的回撥函式來拿到更改之後的值
* 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.
*//setState第一引數是一個function ,他會在未來的某個時間點執行。在執行這個functon時我們都是拿到的最新的元件資訊
*//(比如state,props, context).這些值根尼通過this.state的不一樣,因為function實在receiveProps之後在
*//shouldComponentDupdate之前,所以這些值還沒更新到當前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');
};複製程式碼
上面的註釋告訴我們:
1.setState()第一引數是function也是非同步的
2.function的執行週期是:receiveProps=>function=>shouldComponentUpdate
(prevState, props) => stateChange複製程式碼
如果關於第一引數function的說明還不是很理解,多看幾眼,多想想React生命週期,那就會茅塞頓開 .
言歸正傳,再看看整段程式碼
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');
};複製程式碼
引數:
@param partialState 部分狀態複製程式碼
這個引數或許是整個state,或許只是state其中的一部分 ,最終React合併(就是merge).
state = {name:'myName',age:18} => setState({age:16}/*state的一部分*/)=>state={name:'myName',age:16}複製程式碼
其實就是Object.assign(原始碼:Object.assign({}, prevState, partialState))的理解 .
@param callback 回撥函式,後面專門開一篇來講解複製程式碼
invariant這段程式碼主要對引數作了一些驗證 ,所以 setState()只接受三種型別的引數,比如 object,function和與null 恆等的,比如undefined ,false . 如果你使用這三種判定型別之外的情況,會優雅的提示你錯誤,比如下面這個程式碼 :
this.setState(Symbol());複製程式碼
錯誤資訊如下:
this.updater.enqueueSetState(this, partialState, callback, 'setState');複製程式碼
在這裡 呼叫了 this.updater中的enqueueSetState,看著名字就知道這是一個setState的佇列(準確的說它是一個連結串列),而這個 updater
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;複製程式碼
上面的註釋很清楚地告訴你 我們有個預設的初始值,真正的值其實就在renderer的時候注入進來的,在下一篇文章中我會專門針對這個updater進入深入理解,現在我們來了解下這個updater的結構
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ReplaceState;
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueForceUpdate(inst, callback) {
const fiber = ReactInstanceMap.get(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
};複製程式碼
總結一下:這個setState其實什麼都沒做,它只是簡單的把這個update操作裝入了一個"佇列"裡(看上去是這樣,不是嗎?).
那個問題來了,為什麼它就是非同步的呢?JavaScript不是單執行緒嗎 ?
先拋開React Fiber 架構不談,我們來聊聊 Javascript的 Event Loop ,大家可以在網上找找關於 Event loop,在這裡我重點推薦是 Philip Roberts 在JSConf上關於Eevent loop深情並茂的介紹(YouTube)
那首先大家在網上看到的資料無怪乎把task queue分為microtask和macrotask,同時他們也有個很好聽的名字:微任務和巨集任務(其實是在外語中他們只是用這個詞表示優先順序),巨集任務的優先順序大於微任務。然後會把setTimeout,setInterval等等歸於 macrotask,把promise歸於mircotask.
在這裡我表示,他們的論點放在單個瀏覽器,比如說chrome是對的,但是大家想過沒有,對Promise的原生態支援是ES6(也有超前意識的瀏覽器廠商),實際情況是每個瀏覽器對他們的處理不一樣(每個瀏覽器的程式設計師不一樣嘛),大家可以把相同的程式碼放在不同的瀏覽器,輸出的結果是不一樣的
這裡你只要記住兩點 :
1.所有的native code回撥(window物件上的code)都是 macrotask,比如setTimeout ,Promise,setInterval
2.native code的優先順序要大於普通回撥函式
在React啟示錄裡我有說我們可以"同步"(這裡的同步不是說他直接在stack的呼叫方式,而是看上去同步拿到了結果)拿到state變化後的值,現在把我們上一節部分程式碼作一點修改:
changeValue=()=>{
setTimeout(()=>{
this.setState(
{value:'I have a new value by setTimeout'}
);
console.log(this.state.value);
},0)
new Promise((resolve,reject)=>{
resolve();
}).then(()=>{
this.setState(
{value:'I have a new value by promise '}
);
console.log(this.state.value);
});
};
//result:
I have a new value by promise
I have a new value by setTimeout
複製程式碼
這裡並沒有等待this.setState()佇列執行即可獲得修改後的值,請務必在自己的程式碼中執行,因為我說的可能是錯的。
事實上,setState到這裡已經完成了使命,剩下的所有任務都都交給了這個updater,updater是何方神聖,又是如何工作,它與react 16提出的fiber有什麼樣的關係 ,reconciler又是什麼東西 ?
持續更新中......不要走開,全面瞭解 react的實現原理,不但可以幫助你更好的使用和優化React ,更可以瞭解它的實現架構和設計原理,並運用到實際的專案中 .