基於React 原始碼深入淺出setState:setState非同步實現

墨成發表於2019-03-03

作者 : 墨成

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

3. 基於第二點的說明,再回頭看看官網對function的寫法,我們知道為什麼這個函式的第一引數叫 preState ,而props 就叫props而不是叫preProps的原因了.

(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());複製程式碼

錯誤資訊如下:

基於React 原始碼深入淺出setState:setState非同步實現


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(也有超前意識的瀏覽器廠商),實際情況是每個瀏覽器對他們的處理不一樣(每個瀏覽器的程式設計師不一樣嘛),大家可以把相同的程式碼放在不同的瀏覽器,輸出的結果是不一樣的 

基於React 原始碼深入淺出setState:setState非同步實現

這裡你只要記住兩點 :

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 ,更可以瞭解它的實現架構和設計原理,並運用到實際的專案中 .


相關文章