把setState整明白

加入新團(tuán)隊(duì)后胀滚,團(tuán)隊(duì)項(xiàng)目使用了React Native。剛開(kāi)始接觸React Native茴迁,除了學(xué)習(xí)React Native的使用孔飒,更要了解React.js這個(gè)框架灌闺,才能更好的使用。而React框架中坏瞄,筆者一開(kāi)始就感覺(jué)奇妙的桂对,就是這個(gè)看似同步,表現(xiàn)卻不一定是同步的setState方法鸠匀〗缎保看了網(wǎng)上一些文章,結(jié)論似乎都是從某幾篇博客相互借鑒的結(jié)論缀棍,但里面筆者還是覺(jué)得有一些不太明白的地方宅此,幸虧React.js源碼是開(kāi)源的。順著源碼看下去睦柴,一些困惑的問(wèn)題終于有些眉目诽凌。

開(kāi)始之前毡熏,讀者可思考以下幾個(gè)問(wèn)題:

  • setState是異步還是同步的
  • 在setTimeout方法中調(diào)用setState的值為何馬上就能更新
  • setState中傳入一個(gè)Function為何值馬上就能更新
  • setState為何要如此設(shè)計(jì)
  • setState的最佳實(shí)踐是什么

以下源碼基于我們團(tuán)隊(duì)在用的React 16.0.0版本坦敌,目前最新的React 16.4.0版本的類名和文件結(jié)構(gòu)均有很大變化,但設(shè)計(jì)思想應(yīng)該還是差不多的痢法,可供參考狱窘。

setState的入口

setState的最上層自然在ReactBaseClasses中。

//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
  // ...
  //調(diào)用內(nèi)部updater
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

而這個(gè)updater财搁,在初始化時(shí)已經(jīng)告訴我們蘸炸,是實(shí)際使用時(shí)注入的。

//ReactBaseClasses.js
function ReactComponent(props, context, updater) {
  // ...
  // 真正的updater在renderer注入
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

找到注入的地方尖奔,目的是找到updater是個(gè)什么類型搭儒。

//ReactCompositeComponet.js
mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
        // ...
        // 由Transaction獲取穷当,這個(gè)是ReactReconcileTransaction
        var updateQueue = transaction.getUpdateQueue();
        // ...
        inst.updater = updateQueue;
        // ...
    }
//ReactReconcileTransaction.js
getUpdateQueue: function() {
    return ReactUpdateQueue;
},

終于看到了具體enqueSetState方法的內(nèi)容。

//ReactUpdateQueue.js
enqueueSetState: function(
    publicInstance,
    partialState,
    callback,
    callerName,
  ) {
    // ...
    // 外部示例轉(zhuǎn)化為內(nèi)部實(shí)例
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
    // ...
    // 將需要更新的state放入等待隊(duì)列中
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    // ...
    // callback也一樣放入等待隊(duì)列中
    if (callback !== null) {
      // ...
      if (internalInstance._pendingCallbacks) {
        internalInstance._pendingCallbacks.push(callback);
      } else {
        internalInstance._pendingCallbacks = [callback];
      }
    }

    enqueueUpdate(internalInstance);
  },
      
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

而更新操作由ReactUpdates這個(gè)類負(fù)責(zé)淹禾。

//ReactUpdates.js
function enqueueUpdate(component) {
  //...

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  //...
}

而這個(gè)isBatchingUpdates的判斷馁菜,就是代表是否在批量更新中。如果正在更新中铃岔,則整個(gè)組件放入dirtyComponents數(shù)組中汪疮,后面會(huì)講到。這里這個(gè)batchingStrategy毁习,其實(shí)就是ReactDefaultBatchingStrategy(外部注入的)智嚷。

//ReactDOMStackInjection.js
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);

而這個(gè)類里的,則會(huì)讓掛起更新?tīng)顟B(tài)纺且,并調(diào)用transaction的perform盏道。

//ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 正常情況不會(huì)走入if中
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

Transaction

這里簡(jiǎn)單解釋下事務(wù)(Transaction)的概念,先看源碼中對(duì)事務(wù)的一張解釋圖隆檀。

 //Transaction.js
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

簡(jiǎn)單來(lái)說(shuō)摇天,事務(wù)相當(dāng)于對(duì)某個(gè)方法(anyMethod)執(zhí)行前和執(zhí)行后的多組鉤子的集合。

可以方便的在某個(gè)方法前后分別做一些事情恐仑,而且可以分wrapper定義泉坐,一個(gè)Wrapper一對(duì)鉤子。

具體來(lái)說(shuō)裳仆,可以在Wrapper里定義initialize和close方法腕让,initialize會(huì)在anyMethod執(zhí)行前執(zhí)行,close會(huì)在執(zhí)行后執(zhí)行歧斟。

更新策略里的Transaction

回到剛剛的batchedUpdates方法纯丸,里面那個(gè)transaction其實(shí)執(zhí)行前都是空方法,而callback是外界傳入的enqueueUpdate方法本身静袖,也就是說(shuō)觉鼻,執(zhí)行時(shí)會(huì)被isBatchingUpdates卡住進(jìn)入加入dirtyCompoments中。之后就會(huì)執(zhí)行close方法里面去改變isBatchingUpdates的值和執(zhí)行flushBatchedUpdates方法队橙。

//ReactDefaultBatchingStrategy.js

// 更新?tīng)顟B(tài)isBatchingUpdates的wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

// 真正更新?tīng)顟B(tài)的wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

// 兩個(gè)wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

// 添加transaction的wrappers
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

而這個(gè)flushBatchedUpdates方法坠陈,按照dirtyComonents里的數(shù)量,每次執(zhí)行了一個(gè)transaction捐康。

//ReactUpdates.js
var flushBatchedUpdates = function() {
  while (dirtyComponents.length) {
    var transaction = ReactUpdatesFlushTransaction.getPooled();
    transaction.perform(runBatchedUpdates, null, transaction);
    ReactUpdatesFlushTransaction.release(transaction);
  }
};

而這個(gè)transaction的執(zhí)行前后前后的鉤子如下仇矾。

//ReactUpdtes.js
// 開(kāi)始時(shí)同步dirComponents數(shù)量,結(jié)束時(shí)通過(guò)檢查是否在執(zhí)行中間runBatchedUpdates方法時(shí)還有新加入的component解总,有的話就重新執(zhí)行一遍
var NESTED_UPDATES = {
  initialize: function() {
    this.dirtyComponentsLength = dirtyComponents.length;
  },
  close: function() {
    if (this.dirtyComponentsLength !== dirtyComponents.length) {
      dirtyComponents.splice(0, this.dirtyComponentsLength);
      flushBatchedUpdates();
    } else {
      dirtyComponents.length = 0;
    }
  },
};

var TRANSACTION_WRAPPERS = [NESTED_UPDATES];


//添加wrapper
Object.assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

所以真正更新方法應(yīng)該在runBatchedUpdates中贮匕。

//ReactUpdates.js
function runBatchedUpdates(transaction) {
  
  // 排序,保證父組件比子組件先更新
  dirtyComponents.sort(mountOrderComparator);

  // ...
  for (var i = 0; i < len; i++) {
    
    var component = dirtyComponents[i];
    
    // 這里開(kāi)始進(jìn)入更新組件的方法
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );
  }
}

而ReactReconciler中的performUpateIfNecessary方法只是一個(gè)殼花枫。

performUpdateIfNecessary: function(
    internalInstance,
    transaction,
    updateBatchNumber,
  ) {
    // ...
    internalInstance.performUpdateIfNecessary(transaction);
    // ...
 },

而真正的方法在ReactCompositeComponent中刻盐,如果等待隊(duì)列中有該更新的state掏膏,那么就調(diào)用updateComponent。

//ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
    // ...
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context,
      );
    } else {
    // ...
    }
},

這個(gè)方法判斷了做了一些判斷敦锌,而我們也看到了nextState的值才是最后被更新給state的值壤追。

//ReactCompositeComponent.js
updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
) {
    // 這個(gè)將排序隊(duì)列里的state合并到nextState    
    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;
        
    if (shouldUpdate) {
        this._pendingForceUpdate = false;
        // Will set `this.props`, `this.state` and `this.context`.
        // 這里才正式更新state
        this._performComponentUpdate(
            nextParentElement,
            nextProps,
            nextState,
            nextContext,
            transaction,
            nextUnmaskedContext,
        );
    } else {
      // 這里才正式更新state
      //...
      inst.state = nextState;
      //...
    }
}

_performComponentUpdate: function(
    nextElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    unmaskedContext,
  ) {
    var inst = this._instance;

    var hasComponentDidUpdate = !!inst.componentDidUpdate;
    var prevProps;
    var prevState;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
    }

    // ...

    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    // ...
},

這個(gè)方法也解釋了為什么傳入函數(shù)的state會(huì)更新。

_processPendingState: function(props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    
    //更新了就可以置空了
    this._pendingStateQueue = null;

    
    var nextState = replace ? queue[0] : inst.state;
    var dontMutate = true;
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      //如果setState傳入是函數(shù)供屉,那么接收的state是上輪更新過(guò)的state
      var partial = queue[i];
      let partialState = typeof partial === 'function'
        ? partial.call(inst, nextState, props, context)
        : partial;
      if (partialState) {
        if (dontMutate) {
          dontMutate = false;
          nextState = Object.assign({}, nextState, partialState);
        } else {
          Object.assign(nextState, partialState);
        }
      }
    }
    return nextState;
},

好像setState是同步的耶

而如果按照這個(gè)流程看完行冰,setState應(yīng)該是同步的呀?是哪里出了問(wèn)題呢伶丐。

別急悼做,還記得更新策略里面那個(gè)Transaction么。那里中間調(diào)用的callback是外層傳入的哗魂,也就說(shuō)有可能還有其它調(diào)用了batchedUpdates呢肛走。那么也就是說(shuō),中間的callback录别,并不止setState會(huì)引起朽色。在代碼里搜索后發(fā)現(xiàn),果真還有幾處調(diào)用了batchedUpdates方法组题。

比如ReactMount的這兩個(gè)方法

//ReactMount.js
_renderNewRootComponent: function(
  nextElement,
  container,
  shouldReuseMarkup,
  context,
  callback,
) {
    
    // ...
    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.

    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );
     
    // ...
},
      
unmountComponentAtNode: function(container) {
    // ...   
    ReactUpdates.batchedUpdates(
      unmountComponentFromNode,
      prevComponent,
      container,
    );
    return true;
    // ...
},      

比如ReactDOMEventListener

//ReactDOMEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {   
    // ...
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
     // ...
    }
},
      
//ReactDOMStackInjection.js     
ReactGenericBatching.injection.injectStackBatchedUpdates(
  ReactUpdates.batchedUpdates,
);

所以比如在componentDidMount中直接調(diào)用時(shí)葫男,ReactMount.js 中的_renderNewRootComponent 方法已經(jīng)調(diào)用了,也就是說(shuō)崔列,整個(gè)將 React 組件渲染到 DOM 中的過(guò)程就處于一個(gè)大的 Transaction 中梢褐,而其中的callback沒(méi)有馬上被執(zhí)行,那么自然state沒(méi)有被馬上更新赵讯。

setState為什么這么設(shè)計(jì)

在react中盈咳,state代表UI的狀態(tài),也就是UI由state改變而改變边翼,也就是UI=function(state)鱼响。筆者覺(jué)得,這體現(xiàn)了一種響應(yīng)式的思想组底,而響應(yīng)式與命令式的不同丈积,在于命令式著重看如何命令的過(guò)程,而響應(yīng)式看中數(shù)據(jù)變化如何輸出斤寇。而React中對(duì)Rerender做出的努力桶癣,對(duì)渲染的優(yōu)化拥褂,響應(yīng)式的setState設(shè)計(jì)娘锁,其實(shí)也是其中搭配而不可少的一環(huán)。

最后

最前面的問(wèn)題饺鹃,相信每個(gè)人都有自己的答案莫秆,我這里給出我自己的理解间雀。

Q:setState是異步還是同步的?

A:同步的镊屎,但有時(shí)候是異步的表現(xiàn)惹挟。

Q:在setTimeout方法中調(diào)用setState的值為何馬上就能更新?

A:因?yàn)楸旧砭褪峭降姆觳担矝](méi)有別的因素阻塞连锯。

Q:setState中傳入一個(gè)Function為何值馬上就能更新?

A:源碼中的策略用狱。

Q:setState為何要如此設(shè)計(jì)运怖?

A:為了以響應(yīng)式的方式改變UI。

Q:setState的最佳實(shí)踐是什么夏伊?

A:以響應(yīng)式的思路使用摇展。

參考鏈接

React 源碼剖析系列 - 解密 setState

setState:這個(gè)API設(shè)計(jì)到底怎么樣

setState為什么不會(huì)同步更新組件狀態(tài)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市溺忧,隨后出現(xiàn)的幾起案子咏连,更是在濱河造成了極大的恐慌,老刑警劉巖鲁森,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祟滴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歌溉,警方通過(guò)查閱死者的電腦和手機(jī)踱启,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)研底,“玉大人埠偿,你說(shuō)我怎么就攤上這事“窕蓿” “怎么了冠蒋?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)乾胶。 經(jīng)常有香客問(wèn)我抖剿,道長(zhǎng),這世上最難降的妖魔是什么识窿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任斩郎,我火速辦了婚禮,結(jié)果婚禮上喻频,老公的妹妹穿的比我還像新娘缩宜。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布锻煌。 她就那樣靜靜地躺著妓布,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宋梧。 梳的紋絲不亂的頭發(fā)上匣沼,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音捂龄,去河邊找鬼释涛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛倦沧,可吹牛的內(nèi)容都是我干的枢贿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼刀脏,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼局荚!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起愈污,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤耀态,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后暂雹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體首装,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年杭跪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了仙逻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涧尿,死狀恐怖系奉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情姑廉,我是刑警寧澤缺亮,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站桥言,受9級(jí)特大地震影響萌踱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜号阿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一并鸵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扔涧,春花似錦园担、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至卤档,卻和暖如春蝙泼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劝枣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工汤踏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舔腾。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓溪胶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親稳诚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哗脖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容