標籤:

React源碼分析 - 組件更新與事務

在React中,組件的更新本質上都是由setState操作改變state引起的。因此組件更新的入口在於setState,同樣經過擼源碼和打斷點分析畫了以下的組件更新的流程圖:

setState的定義在組件mountComponent的時候定義:

inst = new Component(publicProps, publicContext, ReactUpdateQueue);function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue;}ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, setState); }};

所以setState的真正的定義在 ReactUpdateQueue.js

enqueueSetState: function (publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, setState); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); // 將state添加到對應的component的_pendingStateQueue數組中。 queue.push(partialState); enqueueUpdate(internalInstance);}enqueueCallback: function (publicInstance, callback, callerName) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance); if (!internalInstance) { return null; } // 將callback添加到對應的component的_pendingCallbacks數組中。 if (internalInstance._pendingCallbacks) { internalInstance._pendingCallbacks.push(callback); } else { internalInstance._pendingCallbacks = [callback]; } enqueueUpdate(internalInstance);}

兩個方法最後都調用enqueueUpdate:

function enqueueUpdate(internalInstance) { ReactUpdates.enqueueUpdate(internalInstance);}function enqueueUpdate(component) { // 確認需要的事務是否注入了。 ensureInjected(); // batchingStrategy.isBatchingUpdates為false的時候, // 或者說當不處於批量更新的時候,用事務的方式批量的進行component的更新。 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 當處於批量更新階段時,不進行state的更新操作,而是將需要更新的component添加到dirtyComponents數組中 dirtyComponents.push(component);}

這裡需要注意enqueueUpdate中根據batchingStrategy.isBatchingUpdates分別進入不同的流程,當isBatchingUpdates為true的時候表示已經處於批量更新的過程中了,這時候會將所有的有改動的組件push到dirtyComponents中。當isBatchingUpdates為false的時候會執行更新操作,這裡先認為當isBatchingUpdates為false的時候進行的操作是更新組件,實際上的過程是更複雜的,稍後馬上解釋具體的過程。這裡我們先理解下React中的事務的概念,事務的概念根據源碼中的注釋就可以非常清楚的了解了:

* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+

簡單來說當使用transaction.perform執行方法method的時候會按順序先執行WRAPPER裡面的initialize方法然後執行method最後再執行close方法。

React提供了基礎的事務對象Transaction,不同的事務的區別就在於initialize和close方法的不同,這個可以通過定義getTransactionWrappers方法來傳入WRAPPER數組,具體的用法看下源碼就好了,不過實際使用中是不會要自己去定義事務的,當然要的話也阻止不了~。

回到enqueueUpdate,其調用的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定義了:

var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { transaction.perform(callback, null, a, b, c, d, e); } }};

可以看到isBatchingUpdates的初始值是false的,在調用batchedUpdates方法的時候會將isBatchingUpdates變數設置為true。然後根據設置之前的isBatchingUpdates的值來執行不同的流程。

對於enqueueUpdate的效果就是,當執行enqueueUpdate的時候如果isBatchingUpdates為true的話(已經處於批量執行操作),則不會進行更新操作,而是將改動的component添加到dirtyComponents數組中;如果isBatchingUpdates為false的話,會執行batchedUpdates將isBatchingUpdates置為true然後調用enqueueUpdate方法,這個時候會用事務的方式來執行enqueueUpdate。

根據流程圖可以知道,事務ReactDefaultBatchingStrategyTransaction的initialize是foo沒有任務操作,接著會執行method即:將改動的組件push到dirtyComponent中,最後執行close方法執行flushBatchedUpdate方法再把isBatchingUpdates重置為false。在flushBatchedUpdates方法中事務執行runBatchedUpdates方法將dirtyComponent中的component依次(先父組件在子組件的順序)進行更新操作。這裡具體的更新的過程看流程圖就可以理解了,需要注意的是updateChildren方法這個方法是virtual DOM的Diff演算法的核心代碼,作用就是根據更新前後組件的不同進行有效的更新,具體的部分,之後單獨的文章再介紹。

在更新的過程中需要注意的一個方法是_processPendingState方法:

_processPendingState: function (props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; this._pendingReplaceState = false; this._pendingStateQueue = null; if (!queue) { return inst.state; } if (replace && queue.length === 1) { return queue[0]; } var nextState = _assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i < queue.length; i++) { var partial = queue[i]; _assign(nextState, typeof partial === function ? partial.call(inst, nextState, props, context) : partial); } return nextState; }

可以看到當setState傳入的是函數的時候,函數被調用的時候的傳入的參數是merge了已經遍歷的queue的state的nextState,如果傳入的不是函數則直接merge state至nextState。這也解釋了,為什麼用回調函數的形式使用setState的時候可以解決state是按照順序最新的state了。

從流程圖可以看到在保證組件更新完畢後會將setState中傳入的callback按照順序依次push到事務的callback queue隊列中,在事務結束的時候close方法中notifyAll就是執行這些callbacks,這樣保證了回調函數是在組件完全更新完成後執行的,也就是setState的回調函數傳入的state是更新後的state的原因。

在了解了以上的組件更新的流程後,可以看一個場景,栗子如下:

import React from react;import ReactDOM from react-dom;import App from ./App;ReactDOM.render(<App />, document.getElementById(root));import React, { Component } from react;import Hello from ./Hello;class App extends Component { constructor(props) { super(props); this.state = { appText: hello App, helloText: heiheihei }; } handleAppClick = () => { console.log(App is clicked ~); this.setState({ appText: App is clicked ~ }); } render() { const { appText, helloText } = this.state; console.log(render App); return ( <div className="app-container"> <div onClick={this.handleAppClick} >{appText}</div> <Hello text={helloText} handleAppClick={this.handleAppClick} /> </div> ); }}export default App;import React, { Component } from react;class Hello extends Component { constructor(props) { super(props); this.state = { text: hello Hello }; } componentWillReceiveProps(nextProps) { this.setState({ text: nextProps.text + ~ }); } handleClick = () => { this.setState({ text: Hello is clicked ~ }); this.props.handleAppClick(); } render() { const { text } = this.state; console.log(render Hello); return ( <div> <div onClick={this.handleClick} stylex={{ color: #e00 }} >{text}</div> </div> ); }}export default Hello;

點擊

hello Hello

後組件的渲染如下,可以看到父組件到子組件按順序更新了一次:

render Apprender Hello

而不是:

render Hellorender Apprender Hello

批量更新的時候組件的順序由:

dirtyComponents.sort(mountOrderComparator);

處理的。

到這裡你需要知道這個結果產生的原因在於不是只有setState的調用棧會改變isBatchingUpdates的值

回顧《React事件機制》的流程圖可以知道事件的統一回調函數dispatchEvent調用了ReactUpdates.batchedUpdates用事務的方式進行事件的處理,也就是說點擊事件的處理本身就是在一個大的事務中,在setState執行的時候isBatchingUpdates已經是true了,setState做的就是將更新都統一push到dirtyComponents數組中,在事務結束的時候按照上述的流程進行批量更新,然後將批量執行關閉結束事務。

事務的機制在這裡也保證了React更新的效率,此外在更新組件的時候的virtual DOM的Diff演算法也起到很大的作用,這個在後續的文章再介紹。

參考資料

  • setState的秘密
  • 深入理解 React 的 batchUpdate 機制

推薦閱讀:

全面了解TCP/IP到HTTP
前端日刊-2018.01.20
我的CSS學習之旅
橫行的前端(上)
對TCP/IP模型的理解

TAG:前端開發 |