React setState 簡單整理總結(jié)

寫業(yè)務(wù)代碼的時(shí)候 需要經(jīng)常用到setState, 前幾天review代碼的時(shí)候, 又想了一下這個(gè)API, 發(fā)現(xiàn)對它的了解不是很清楚, 僅僅是 setState 是異步的, 周六在家參考了一些資料,簡單整理了下,寫的比較簡單, 通篇閱讀大概耗時(shí) 5min, 在這簡單分享一下, 希望對大家有所幫助 ;)。

先看一個(gè)例子

假如有這樣一個(gè)點(diǎn)擊執(zhí)行累加場景:

// …
this.state = {
  count: 0,
}

incrementCount() {
  this.setState({
    count: this.state.count + 1,
  });
}

handleIncrement = () => {
 this.incrementCount();
 this.incrementCount();
 this.incrementCount();
}
// ..

每一次點(diǎn)擊, 累加三次,看一下輸入:

并沒有達(dá)到預(yù)期的效果,糾正也很簡單:

incrementCount() {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

再看輸出:

setState 的時(shí)候, 一個(gè)傳入了object, 一個(gè)傳入了更新函數(shù)。

區(qū)別在于: 傳入一個(gè)更新函數(shù),就可以訪問當(dāng)前狀態(tài)值。 setState調(diào)用是 批量處理的,因此可以讓更新建立在彼此之上,避免沖突。

那問題來了, 為什么前一種方式就不行呢? 帶著這個(gè)疑問,繼續(xù)往下看。

setState為什么不會(huì)同步更新組件?
進(jìn)入這個(gè)問題之前,我們先回顧一下現(xiàn)在對setState的認(rèn)知:
1.setState不會(huì)立刻改變React組件中state的值.
2.setState通過觸發(fā)一次組件的更新來引發(fā)重繪.
3.多次setState函數(shù)調(diào)用產(chǎn)生的效果會(huì)合并。

重繪指的就是引起React的更新生命周期函數(shù)4個(gè)函數(shù):

  • shouldComponentUpdate(被調(diào)用時(shí)this.state沒有更新;如果返回了false,生命周期被中斷,雖然不調(diào)用之后的函數(shù)了,但是state仍然會(huì)被更新)
  • componentWillUpdate(被調(diào)用時(shí)this.state沒有更新)
  • render(被調(diào)用時(shí)this.state得到更新)
  • componentDidUpdate

如果每一次setState調(diào)用都走一圈生命周期,光是想一想也會(huì)覺得會(huì)帶來性能的問題,其實(shí)這四個(gè)函數(shù)都是純函數(shù),性能應(yīng)該還好,但是render函數(shù)返回的結(jié)果會(huì)拿去做Virtual DOM比較和更新DOM樹,這個(gè)就比較費(fèi)時(shí)間。

目前React會(huì)將setState的效果放在隊(duì)列中,積攢著一次引發(fā)更新過程。
為的就是把Virtual DOM和DOM樹操作降到最小,用于提高性能。

查閱一些資料后發(fā)現(xiàn),某些操作還是可以同步更新this.state的。

setState 什么時(shí)候會(huì)執(zhí)行同步更新?
先直接說結(jié)論吧:
在React中,如果是由React引發(fā)的事件處理(比如通過onClick引發(fā)的事件處理),調(diào)用setState不會(huì)同步更新this.state,除此之外的setState調(diào)用會(huì)同步執(zhí)行this.state。

所謂“除此之外”,指的是繞過React通過
addEventListener
直接添加的事件處理函數(shù),還有通過
setTimeout || setInterval

產(chǎn)生的異步調(diào)用。

簡單一點(diǎn)說, 就是經(jīng)過React 處理的事件是不會(huì)同步更新this.state的. 通過 addEventListener || setTimeout/setInterval 的方式處理的則會(huì)同步更新。
具體可以參考 jsBin 的這個(gè)例子。

結(jié)果就很清晰了:

點(diǎn)擊Increment ,執(zhí)行onClick ,輸出0;
而通過addEventListener , 和 setTimeout 方式處理的, 第一次 直接輸出了1;

理論大概是這樣的,盜用一張圖:

image.png

在React的setState函數(shù)實(shí)現(xiàn)中,會(huì)根據(jù)一個(gè)變量 isBatchingUpdates 判斷是 直接更新 this.state還是 放到隊(duì)列 中。

而isBatchingUpdates默認(rèn)是false,也就表示setState會(huì)同步更新this.state,但是有一個(gè)函數(shù)batchedUpdates。

這個(gè)函數(shù)會(huì)把isBatchingUpdates修改為true,而當(dāng)React在調(diào)用事件處理函數(shù)之前就會(huì)調(diào)用這個(gè)batchedUpdates,造成的后果,就是由React控制的事件處理過程setState不會(huì)同步更新this.state。

通過上圖,我們知道了大致流程, 要想徹底了解它的機(jī)制,我們解讀一下源碼。

探秘setState 源碼
// setState方法入口如下:
ReactComponent.prototype.setState = function (partialState, callback) {
// 將setState事務(wù)放入隊(duì)列中
this.updater.enqueueSetState(this, partialState);
if (callback) {

this.updater.enqueueCallback(this, callback, 'setState');

}};

相關(guān)的幾個(gè)概念:
partialState,有部分state的含義,可見只是影響涉及到的state,不會(huì)傷及無辜。
enqueueSetState 是 state 隊(duì)列管理的入口方法,比較重要,我們之后再接著分析。

replaceState
replaceState: function (newState, callback) {
this.updater.enqueueReplaceState(this, newState);
if (callback) {

this.updater.enqueueCallback(this, callback, 'replaceState');

}},
replaceState中取名為newState,有完全替換的含義。同樣也是以隊(duì)列的形式來管理的。

enqueueSetState
enqueueSetState: function (publicInstance, partialState) {

// 先獲取ReactComponent組件對象
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

if (!internalInstance) {
  return;
}

// 如果_pendingStateQueue為空,則創(chuàng)建它。可以發(fā)現(xiàn)隊(duì)列是數(shù)組形式實(shí)現(xiàn)的
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);

// 將要更新的ReactComponent放入數(shù)組中
enqueueUpdate(internalInstance);}

其中g(shù)etInternalInstanceReadyForUpdate源碼如下

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
// 從map取出ReactComponent組件,還記得mountComponent時(shí)把ReactElement作為key,將ReactComponent存入了map中了吧,ReactComponent是React組件的核心,包含各種狀態(tài),數(shù)據(jù)和操作方法。而ReactElement則僅僅是一個(gè)數(shù)據(jù)類。
var internalInstance = ReactInstanceMap.get(publicInstance);
if (!internalInstance) {

return null;

}

return internalInstance;}

enqueueUpdate源碼如下:
function enqueueUpdate(component) {
ensureInjected();

// 如果不是正處于創(chuàng)建或更新組件階段,則處理update事務(wù)
if (!batchingStrategy.isBatchingUpdates) {

batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;

}

// 如果正在創(chuàng)建或更新組件,則暫且先不處理update,只是將組件放在dirtyComponents數(shù)組中
dirtyComponents.push(component);}

batchedUpdates
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 批處理最開始時(shí),將isBatchingUpdates設(shè)為true,表明正在更新
ReactDefaultBatchingStrategy.isBatchingUpdates = true;

<ins data-ad-format="auto" class="adsbygoogle adsbygoogle-noablate" data-ad-client="ca-pub-6330872677300335" data-adsbygoogle-status="done" style="box-sizing: border-box; display: block; margin: auto; background-color: transparent;"><ins id="aswift_6_expand" style="box-sizing: border-box; display: inline-table; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 825px; background-color: transparent;"><ins id="aswift_6_anchor" style="box-sizing: border-box; display: block; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 825px; background-color: transparent; overflow: hidden; opacity: 0;"><iframe width="825" height="200" frameborder="0" marginwidth="0" marginheight="0" vspace="0" hspace="0" allowtransparency="true" scrolling="no" allowfullscreen="true" onload="var i=this.id,s=window.google_iframe_oncopy,H=s&&s.handlers,h=H&&H[i],w=this.contentWindow,d;try{d=w.document}catch(e){}if(h&&d&&(!d.body||!d.body.firstChild)){if(h.call){setTimeout(h,0)}else if(h.match){try{h=s.upd(h,i)}catch(e){}w.location.replace(h)}}" id="aswift_6" name="aswift_6" style="box-sizing: border-box; left: 0px; position: absolute; top: 0px; border: 0px; width: 825px; height: 200px;"></iframe></ins></ins></ins>

// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {

callback(a, b, c, d, e);

} else {

// 以事務(wù)的方式處理updates,后面詳細(xì)分析transaction
transaction.perform(callback, null, a, b, c, d, e);

}}
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {

// 事務(wù)批更新處理結(jié)束時(shí),將isBatchingUpdates設(shè)為了false
ReactDefaultBatchingStrategy.isBatchingUpdates = false;

}};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

enqueueUpdate包含了React避免重復(fù)render的邏輯。

mountComponent 和 updateComponent方法在執(zhí)行的最開始,會(huì)調(diào)用到batchedUpdates進(jìn)行批處理更新,此時(shí)會(huì)將isBatchingUpdates設(shè)置為true,也就是將狀態(tài)標(biāo)記為現(xiàn)在正處于更新階段了。

之后React以事務(wù)的方式處理組件update,事務(wù)處理完后會(huì)調(diào)用wrapper.close() 。

而TRANSACTION_WRAPPERS中包含了RESET_BATCHED_UPDATES這個(gè)wrapper,故最終會(huì)調(diào)用RESET_BATCHED_UPDATES.close(), 它最終會(huì)將isBatchingUpdates設(shè)置為false。

故 getInitialState,componentWillMount, render,componentWillUpdate 中 setState 都不會(huì)引起 updateComponent。

但在componentDidMount 和 componentDidUpdate中則會(huì)。

事務(wù)
事務(wù)通過wrapper進(jìn)行封裝。

一個(gè)wrapper包含一對 initialize 和 close 方法。比如RESET_BATCHED_UPDATES:

var RESET_BATCHED_UPDATES = {
// 初始化調(diào)用
initialize: emptyFunction,
// 事務(wù)執(zhí)行完成,close時(shí)調(diào)用
close: function () {

ReactDefaultBatchingStrategy.isBatchingUpdates = false;

}};

transcation被包裝在wrapper中,比如:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

transaction是通過transaction.perform(callback, args…)方法進(jìn)入的,它會(huì)先調(diào)用注冊好的wrapper中的initialize方法,然后執(zhí)行perform方法中的callback,最后再執(zhí)行close方法。

下面分析transaction.perform(callback, args…)

perform: function (method, scope, a, b, c, d, e, f) {

var errorThrown;
var ret;
try {
  this._isInTransaction = true;
  errorThrown = true;
  // 先運(yùn)行所有wrapper中的initialize方法
  this.initializeAll(0);

  // 再執(zhí)行perform方法傳入的callback
  ret = method.call(scope, a, b, c, d, e, f);
  errorThrown = false;
} finally {
  try {
    if (errorThrown) {
      // 最后運(yùn)行wrapper中的close方法
      try {
        this.closeAll(0);
      } catch (err) {}
    } else {
      // 最后運(yùn)行wrapper中的close方法
      this.closeAll(0);
    }
  } finally {
    this._isInTransaction = false;
  }
}
return ret;

},

initializeAll: function (startIndex) {

var transactionWrappers = this.transactionWrappers;
// 遍歷所有注冊的wrapper
for (var i = startIndex; i < transactionWrappers.length; i++) {
  var wrapper = transactionWrappers[i];
  try {
    this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
    // 調(diào)用wrapper的initialize方法
    this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
  } finally {
    if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
      try {
        this.initializeAll(i + 1);
      } catch (err) {}
    }
  }
}

},

closeAll: function (startIndex) {

var transactionWrappers = this.transactionWrappers;
// 遍歷所有wrapper
for (var i = startIndex; i < transactionWrappers.length; i++) {
  var wrapper = transactionWrappers[i];
  var initData = this.wrapperInitData[i];
  var errorThrown;
  try {
    errorThrown = true;
    if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
      // 調(diào)用wrapper的close方法,如果有的話
      wrapper.close.call(this, initData);
    }
    errorThrown = false;
  } finally {
    if (errorThrown) {
      try {
        this.closeAll(i + 1);
      } catch (e) {}
    }
  }
}
this.wrapperInitData.length = 0;

}

更新組件: runBatchedUpdates
前面分析到enqueueUpdate中調(diào)用transaction.perform(callback, args...)后,發(fā)現(xiàn),callback還是enqueueUpdate方法啊,那豈不是死循環(huán)了?不是說好的setState會(huì)調(diào)用updateComponent,從而自動(dòng)刷新View的嗎? 我們還是要先從transaction事務(wù)說起。

我們的wrapper中注冊了兩個(gè)wrapper,如下:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

RESET_BATCHED_UPDATES用來管理isBatchingUpdates狀態(tài),我們前面在分析setState是否立即生效時(shí)已經(jīng)講解過了。

那FLUSH_BATCHED_UPDATES用來干嘛呢?

var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)};
var flushBatchedUpdates = function () {
// 循環(huán)遍歷處理完所有dirtyComponents
while (dirtyComponents.length || asapEnqueued) {

if (dirtyComponents.length) {
  var transaction = ReactUpdatesFlushTransaction.getPooled();
  // close前執(zhí)行完runBatchedUpdates方法,這是關(guān)鍵
  transaction.perform(runBatchedUpdates, null, transaction);
  ReactUpdatesFlushTransaction.release(transaction);
}

if (asapEnqueued) {
  asapEnqueued = false;
  var queue = asapCallbackQueue;
  asapCallbackQueue = CallbackQueue.getPooled();
  queue.notifyAll();
  CallbackQueue.release(queue);
}

}};

FLUSH_BATCHED_UPDATES會(huì)在一個(gè)transaction的close階段運(yùn)行runBatchedUpdates,從而執(zhí)行update。

function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
dirtyComponents.sort(mountOrderComparator);

for (var i = 0; i < len; i++) {

// dirtyComponents中取出一個(gè)component
var component = dirtyComponents[i];

// 取出dirtyComponent中的未執(zhí)行的callback,下面就準(zhǔn)備執(zhí)行它了
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;

var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
  var namedComponent = component;
  if (component._currentElement.props === component._renderedComponent._currentElement) {
    namedComponent = component._renderedComponent;
  }
}
// 執(zhí)行updateComponent
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

// 執(zhí)行dirtyComponent中之前未執(zhí)行的callback
if (callbacks) {
  for (var j = 0; j < callbacks.length; j++) {
    transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
  }
}

}}

runBatchedUpdates循環(huán)遍歷dirtyComponents數(shù)組,主要干兩件事。

  • 首先執(zhí)行performUpdateIfNecessary來刷新組件的view
  • 執(zhí)行之前阻塞的callback。

下面來看performUpdateIfNecessary:

performUpdateIfNecessary: function (transaction) {

if (this._pendingElement != null) {
  // receiveComponent會(huì)最終調(diào)用到updateComponent,從而刷新View
  ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
}

if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
  // 執(zhí)行updateComponent,從而刷新View。這個(gè)流程在React生命周期中講解過
  this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
}

},

最后驚喜的看到了receiveComponent和updateComponent吧。

receiveComponent最后會(huì)調(diào)用updateComponent,而updateComponent中會(huì)執(zhí)行React組件存在期的生命周期方法,

如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。

從而完成組件更新的整套流程。

整體流程回顧:
1.enqueueSetState將state放入隊(duì)列中,并調(diào)用enqueueUpdate處理要更新的Component
2.如果組件當(dāng)前正處于update事務(wù)中,則先將Component存入dirtyComponent中。否則調(diào)用batchedUpdates處理。
3.batchedUpdates發(fā)起一次transaction.perform()事務(wù)
4.開始執(zhí)行事務(wù)初始化,運(yùn)行,結(jié)束三個(gè)階段
5.初始化:事務(wù)初始化階段沒有注冊方法,故無方法要執(zhí)行
6.運(yùn)行:執(zhí)行setSate時(shí)傳入的callback方法,一般不會(huì)傳callback參數(shù)
7.結(jié)束:更新isBatchingUpdates為false,并執(zhí)行FLUSH_BATCHED_UPDATES這個(gè)wrapper中的close方法
8.FLUSH_BATCHED_UPDATES在close階段,會(huì)循環(huán)遍歷所有的dirtyComponents,調(diào)用updateComponent刷新組件,并執(zhí)行它的pendingCallbacks, 也就是setState中設(shè)置的callback。

看完理論, 我們再用一個(gè)例子鞏固下:
再看一個(gè)例子:

class Example extends React.Component {
constructor() {
super();
this.state = {


val: 0


};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log('第 1 次 log:', this.state.val);
this.setState({val: this.state.val + 1});
console.log('第 2 次 log:', this.state.val);

setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log('第 3 次 log:', this.state.val); 
this.setState({val: this.state.val + 1});
console.log('第 4 次 log:', this.state.val); 
}, 0);
}
render() {
return null;
}
};

前兩次在isBatchingUpdates 中,沒有更新state, 輸出兩個(gè)0。
后面兩次會(huì)同步更新, 分別輸出2, 3;

很顯然,我們可以將4次setState簡單規(guī)成兩類:
componentDidMount是一類
setTimeOut中的又是一類,因?yàn)檫@兩次在不同的調(diào)用棧中執(zhí)行。

我們先看看在componentDidMount中setState的調(diào)用棧:

再看看在setTimeOut中的調(diào)用棧:

我們重點(diǎn)看看在componentDidMount中的sw3e調(diào)用棧 :
發(fā)現(xiàn)了batchedUpdates方法。

原來在setState調(diào)用之前,就已經(jīng)處于batchedUpdates執(zhí)行的事務(wù)之中了。

那batchedUpdates方法是誰調(diào)用的呢?我們再往上追溯一層,原來是ReactMount.js中的_renderNewRootComponent方法。

也就是說,整個(gè)將React組件渲染到DOM的過程就處于一個(gè)大的事務(wù)中了。

接下來就很容易理解了: 因?yàn)樵赾omponentDidMount中調(diào)用setState時(shí),batchingStrategy的isBatchingUpdates已經(jīng)被設(shè)置為true,所以兩次setState的結(jié)果并沒有立即生效,而是被放進(jìn)了dirtyComponents中。

這也解釋了兩次打印this.state.val都是0的原因,因?yàn)樾碌膕tate還沒被應(yīng)用到組件中。

再看setTimeOut中的兩次setState,因?yàn)闆]有前置的batchedUpdate調(diào)用,所以batchingStrategy的isBatchingUpdates標(biāo)志位是false,也就導(dǎo)致了新的state馬上生效,沒有走到dirtyComponents分支。

也就是說,setTimeOut中的第一次執(zhí)行,setState時(shí),this.state.val為1;
而setState完成后打印時(shí)this.state.val變成了2。

第二次的setState同理。

通過上面的例子,我們就知道setState 是可以同步更新的,但是還是盡量避免直接使用, 僅作了解就可以了。

如果你非要玩一些騷操作,寫出這樣的代碼去直接去操作this.state:
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.setState();

我只能說, 大胸弟, 你很騷。吾有舊友叼似汝,而今墳草丈許高。

結(jié)語
最后簡單重復(fù)下結(jié)論吧:

  • 不要直接去操作this.state, 這樣會(huì)造成不必要的性能問題和隱患。
  • 由React引發(fā)的事件處理,調(diào)用setState不會(huì)同步更新this.state,除此之外的setState調(diào)用會(huì)同步執(zhí)行this.state。

我對這一套理論也不是特別熟悉, 如有紕漏, 歡迎指正 :)

擴(kuò)展閱讀
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...

https://medium.com/@wisecobbl...

https://zhuanlan.zhihu.com/p/...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容