上篇: https://segmentfault.com/a/11...
在React中,若是是由React引起的事件處理(好比經過onClick引起的合成事件處理)和組件生命週期函數內(好比componentDidMount),調用this.setState不會同步更新this.state,除此以外的setState調用會同步執行this.state。
所謂「除此以外」,指的是繞過React經過addEventListener直接添加的事件處理函數,還有經過setTimeout/setInterval產生的異步調用。react
若是咱們按照教科書般的方式來使用React,基本上不會觸及所謂的「除此以外」狀況。
在React的setState函數實現中,會根據一個變量isBatchingUpdates
判斷是直接更新this.state仍是放到隊列中回頭再說,而isBatchingUpdates
默認是false
,也就表示setState會同步更新this.state,可是,有一個函數batchedUpdates
,這個函數會把isBatchingUpdates
修改成true,而當React在調用事件處理函數和自身生命週期以前就會調用這個batchedUpdates
,形成的後果,就是由React控制的事件處理過程和生命週期中的同步代碼調用
的setState
不會同步更新this.state
。webpack
注意:同步代碼調用,在合成事件和生命週期內的異步調用setState(好比ajax和setTimeout內),也是會同步更新this.setState。
Demo請看上篇的Part Fourgit
因此按照正常React用法都是會通過batchingUpdate方法的。這是因爲React有一套自定義的事件系統和生命週期流程控制,使用原生事件監聽和settimeout這種方式會跳出React這個體系,因此會直接更新this.state。github
咱們在看代碼是如何實現的,須要瞭解這樣一個東西「事務」,React內部的工具方法實現了一個可供使用的事務。web
React中的事務借用了計算機專業術語的單詞Transaction
。對比數據庫的事務性質,二者之間有共同點卻又不是一回事,簡答來講 把須要執行的方法用一個容器封裝起來,在容器內執行方法的先後,分別執行init方法和close方,其次來講,一個容器能夠包裹另外一個容器,這點又相似於洋蔥模型。ajax
React的合成事件系統和生命週期就使用了React內部實現的事務,爲其函數附加了先後兩個相似npm腳本pre和post兩個鉤子的事件。數據庫
這是一個npm srcipt
的例子:npm
"prebuild": "echo I run before the build script", "build": "cross-env NODE_ENV=production webpack", "postbuild": "echo I run after the build script" //用戶執行npm run build就會實際執行 npm run prebuild && npm run build && npm run postbuild //所以能夠在兩個鉤子裏作一些準備工做和清理工做。
有過有興趣,咱們來看一下如何簡單使用事務:https://codesandbox.io/s/6xl5yrjvzzsegmentfault
var MyTransaction = function () { //... }; Object.assign(MyTransaction.prototype, Transaction.Mixin, { getTransactionWrappers: function () { return [ { initialize: function () { console.log("before method perform"); }, close: function () { console.log("after method perform"); } } ]; } }); var transaction = new MyTransaction(); transaction.reinitializeTransaction() var testMethod = function () { console.log("test"); }; transaction.perform(testMethod);
因此,咱們能夠獲得啓發,React的事件系統和生命週期事務先後的鉤子對isBatchingUpdates作了修改,其實就是在事務的前置pre內調用了batchedUpdates方法修改了變量爲true,而後在後置鉤子又置爲false,而後發起真正的更新檢測,而事務中異步方法運行時候,因爲JavaScript的異步機制,異步方法(setTimeout等)其中的setState運行時候,同步的代碼已經走完,後置鉤子已經把isBatchingUpdates設爲false,因此此時的setState會直接進入非批量更新模式,表如今咱們看來成爲了同步SetState。數組
嘗試在描述一下:整個React的每一個生命週期和合成事件都處在一個大的事務當中。原生綁定事件和setTimeout異步的函數沒有進入React的事務當中,或者是當他們執行時,剛剛的事務已經結束了,後置鉤子觸發了,close了。(你們能夠想想分別是哪種狀況)。
React「坐」在頂部調用堆棧框架並知道全部React事件處理程序什麼時候運行,setState在React管理的合成事件或者生命週期中調用,它會啓用批量更新事務,進入了批量更新模式,全部的setState的改變都會暫存到一個隊列,延遲到事務結束再合併更新。若是setState在React的批量更新事務外部或者以後調用,則會當即刷新。
懂得了事務,再回看,就明白,其實setState歷來都是同步運行,不過是React利用事務工具方法模擬了setState異步的假象。
延遲隊列如何實現,其實有悟性的同窗已經能夠大概猜到。咱們再來捋一捋,看源碼是否能驗證咱們的結論描述和現象。
能夠對照這張圖先來看下setState的流程代碼,代碼倉庫在個人github的react-source倉庫,目錄已被我精簡,剩下關鍵的源代碼文件夾。
首先,咱們搜索setState =
看下setState何處被賦值,找到了這裏
// src/isomorphic/modern/class/ReactComponent.js /* * React組件繼承自React.Component,而setState是React.Component的方法, * 所以對於組件來說setState屬於其原型方法,首先看setState的定義: */ ReactComponent.prototype.setState = function(partialState, callback) { // 忽略掉入參驗證和開發拋錯 //調用setState實際是調用了enqueueSetState // 調用隊列的入隊方法,把當前組件的示例和state存進入 this.updater.enqueueSetState(this, partialState); if (callback) { // 若是有回調,把回調存進setState隊列的後置鉤子 this.updater.enqueueCallback(this, callback, 'setState'); } };
會發現調用setState實際是調用this.updater.enqueueSetState,此時咱們不得不看一看updater及其enqueueSetState方法是什麼東西,咱們在當前文件搜索:
function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; // updater有默認值,真實運行時會注入,其實也算依賴注入 this.updater = updater || ReactNoopUpdateQueue; }
ReactNoopUpdateQueue是一個這樣的對象,提供了基本的無效方法,真正的updater只有在React被真正加載前纔會被注入進來,運行時注入,嚴格來講是依賴注入,是React源碼的風格之一。
// src/isomorphic/modern/class/ReactNoopUpdateQueue.js var ReactNoopUpdateQueue={ isMounted: function(publicInstance) { return false; }, enqueueCallback: function(publicInstance, callback) { }, enqueueForceUpdate: function(publicInstance) { }, enqueueReplaceState: function(publicInstance, completeState) { }, enqueueSetState: function(publicInstance, partialState) { }, }
真實的enqueueSetState在這個文件內,方法把將要修改的state存入組件實例的internalInstance數組中,這裏就是state的延遲更新隊列了。而後立馬調用了一個全局的ReactUpdates.enqueueUpdate(internalInstance)方法。
// src/renderers/shared/reconciler/ReactUpdateQueue.js // 這個是setState真正調用的函數 enqueueSetState: function(publicInstance, partialState) { // 忽略基本的容錯和拋錯 // 存入組件實例,準備更新 var internalInstance = publicInstance; // 更新隊列合併操做 更新 internalInstance._pendingStateQueue var queue = internalInstance._pendingStateQueue ||(internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); // 發生了什麼?猜一下? ReactUpdates.js },
題外話:每一個函數的健壯入參判斷和運行環境判斷和完善的拋錯機制,對來源不信任甚至是內部互調,是React對開發者友好的一個重要緣由。
咱們來猜下ReactUpdates.enqueueUpdate幹了什麼?根據上面的流程圖我猜測應當是判斷流程。
function enqueueUpdate(component) { ensureInjected(); //環境判斷:是否有調度事務方法同時有批量更新策略方法 //關鍵的判斷條件,是不是批量更新 //但是isBatchingUpdates這個值誰來維護呢? if (!batchingStrategy.isBatchingUpdates) { // 見詞知意 若是不在批量更新策略中 // 若是不是批量更新,猜測一下,應該會當即更新吧? // 唉?batchingStrategy到底在作什麼呢 batchingStrategy.batchedUpdates(enqueueUpdate, component); // 調用事務 // 對隊列中的更新執行 batchedUpdates 方法 return; } // 若是是批量更新,那就把組件放入髒組件隊列,也就是待更新組件隊列 dirtyComponents.push(component); }
須要看ReactDefaultBatchingStrategy.js 看 batchedUpdates 方法,這個js文件就有意思了,一上來就是咱們以前提到的事務。
避免枯燥,我用人話闡述一下這個js的內容,也能夠直接看ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy={ isBatchingUpdates:false, batchedUpdates:function(){}, }
文件底部聲明瞭 ReactDefaultBatchingStrategy對象,內部isBatchingUpdates初始值爲false,這個就是咱們心心念念判斷是否在批量更新策略的重要變量。
這個isBatchingUpdates變量搜索整個項目,發現它只被兩處改變:
實質上,isBatchingUpdates僅僅也就是被batchedUpdates方法維護着,batchedUpdates調用時開啓批量更新,同時入參callback被事務包裹調用,callback調用完成時候事務close鉤子觸發,關閉批量更新模式。事務的close鉤子函數有兩個,另外一個以前會調用ReactUpdates.flushBatchedUpdates方法,也就是真正的把積攢的setState隊列進行更新計算。
問題來了,callback是啥,batchedUpdates方法在setState以前,或者說除了setState還會被誰調用,致使isBatchingUpdates變爲true,我猜測是生命週期函數和合成事件,只有這樣,整個維護批量更新策略的機制就造成了閉環,驗證了咱們以前的結論。
咱們搜索batchedUpdates(
,果不其然,在src/renderers/dom/client/ReactEventListener.js
和src/renderers/dom/client/ReactMount.js
中找到了ReactUpdates.batchedUpdates的調用。
合成事件和生命週期的裝載發生時,調用了batchedUpdates
方法,使得內部的同步代碼均可以運行在批量更新策略的事務環境中,結束後,便使用事務的後置鉤子啓動merge更新,重置常量。
另外我在ReactDOM.js
發現了對React頂層API對batchedUpdates方法的引用,可讓 Promise 這些異步也能進入 batch update:
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
另外一個彩蛋,雖然React不提倡使用這個API,之後版本也可能移除,可是如今咱們能夠這樣在React中這樣使用:
React.unstable_batchedUpdates(function(){ this.setState({...}) this.setState({...}) //...在此函數內也可使用批量更新策略 })
解決了setTimeout和AJAX異步方法、原生事件內的setState批量更新策略失效的問題,讓批量更新在任何場景都會發生。