說在前面,着重梳理實際更新組件和 dom 部分的代碼,可是關於異步,transaction,批量合併新狀態等新細節只描述步驟。一來由於這些細節在讀源碼的時候只讀了部分,二來若是要把這些都寫出來要寫老長老長。javascript
真實的 setState 的過程:html
setState( partialState ) { // 1. 經過組件對象獲取到渲染對象 var internalInstance = ReactInstanceMap.get(publicInstance); // 2. 把新的狀態放在渲染對象的 _pendingStateQueue 裏面 internalInstance._pendingStateQueue.push( partialState ) // 3. 查看下是否正在批量更新 // 3.1. 若是正在批量更新,則把當前這個組件認爲是髒組件,把其渲染對象保存到 dirtyComponents 數組中 // 3.2. 若是能夠批量更新,則調用 ReactDefaultBatchingStrategyTransaction 開啓更新事務,進行真正的 vdom diff。 // | // v // internalInstance.updateComponent( partialState ) }
updateComponent 方法的說明:java
updateComponent( partialState ) { // 源碼中 partialState 是從 this._pendingStateQueue 中獲取的,這裏簡化了狀態隊列的東西,假設直接從外部傳入 var inst = this._instance; var nextState = Object.assign( {}, inst.state, partialState ); // 得到組件對象,準備更新,先調用生命週期函數 // 調用 shouldComponentUpdate 看看是否須要更新組件(這裏先忽略 props 和 context的更新) if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) { // 更新前調用 componentWillUpdate isnt.componentWillUpdate( inst.props, nextState, nextContext ); inst.state = nextState; // 生成新的 vdom var nextRenderedElement = inst.render(); // 經過上一次的渲染對象獲取上一次生成的 vdom var prevComponentInstance = this._renderedComponent; // render 中的根節點的渲染對象 var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根節點的 vdom // 經過比較新舊 vdom node 來決定是更新 dom node 仍是根據最新的 vdom node 生成一份真實 dom node 替換掉原來的 if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) { // 更新 dom node prevComponentInstance.receiveComponent( nextRenderedElement ) } else { // 生成新的 dom node 替換原來的(如下是簡化版,只爲了說明流程) var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance ); // 根據新的 vdom 生成新的渲染對象 var child = instantiateReactComponent( nextRenderedElement ); this._renderedComponent = child; // 生成新的 dom node var nextMarkup = child.mountComponent(); // 替換原來的 dom node oldHostNode.empty(); oldHostNode.appendChild( nextMarkup ) } } }
接下來看下 shouldUpdateReactComponent 方法:node
function shouldUpdateReactComponent(prevElement, nextElement) { var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; } var prevType = typeof prevElement; var nextType = typeof nextElement; if (prevType === 'string' || prevType === 'number') { return (nextType === 'string' || nextType === 'number'); } else { return ( nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key ); } }
基本的思路就是比較當前 vdom 節點的類型,若是一致則更新,若是不一致則從新生成一份新的節點替換掉原來的。好了回到剛剛跟新 dom node這條路 prevComponentInstance.receiveComponent( nextRenderedElement ),即 render 裏面根元素的渲染對象的 receiveComponent 方法作了最後的更新 dom 的工做。若是根節點的渲染對象是組件即 ReactCompositeComponent.receiveComponent,若是根節點是內置對象(html 元素)節點即 ReactDOMComponent.receiveComponent。ReactCompositeComponent.receiveComponent 最終仍是調用的上面提到的 updateComponent 循環去生成 render 中的 vdom,這裏就先不深究了。最終 html dom node 的更新策略都在 ReactDOMComponent.receiveComponent 中。react
class ReactDOMComponent { // @param {nextRenderedElement} 新的 vdom node receiveComponent( nextRenderedElement ) { var prevElement = this._currentElement; this._currentElement = nextRenderedElement; var lastProps = prevElement.props; var nextProps = this._currentElement.props; var lastChildren = lastProps.children; var nextChildren = nextProps.children; /* 更新 props _updateDOMProperties 方法作了下面兩步 1. 記錄下 lastProps 中有的,nextProps 沒有的,刪除 2. 記錄下 nextProps 中有的,且與 lastProps中不一樣的屬性,setAttribute 之 */ this._updateDOMProperties(lastProps, nextProps, transaction); /* 迭代更新子節點,源代碼中是 this._updateDOMChildren(lastProps, nextProps, transaction, context); 如下把 _updateDOMChildren 方法展開,對於子節點類型的判斷源碼比較複雜,這裏只針對string|number和非string|number作一個簡單的流程示例 */ // 1. 若是子節點從有到無,則刪除子節點 if ( lastChildren != null && nextChildren == null ) { if ( typeof lastChildren === 'string' | 'number' /* 僞代碼 */ ) { this.updateTextContent(''); } else { this.updateChildren( null, transaction, context ); } } // 2. 若是新的子節點相對於老的是有變化的 if ( nextChildren != null ) { if ( typeof lastChildren === 'string' | 'number' && lastChildren !== nextChildren /* 僞代碼 */ ) { this.updateTextContent('' + nextChildren); } else if ( lastChildren !== nextChildren ) { this.updateChildren( nextChildren, transaction, context ); } } } }
this.updateChildren( nextChildren, transaction, context )
中是真正的 diff 算法,就不以代碼來講了(由於光靠代碼很難說明清楚)算法
先來看最簡單的狀況:
例A:
按節點順序開始遍歷 nextChildren(遍歷的過程當中記錄下須要對節點作哪些變動,等遍歷完統一執行最終的 dom 操做),相同位置若是碰到和 prevChildren 中 tag 同樣的元素認爲不須要對節點進行刪除,只須要更新節點的 attr,若是碰到 tag 不同,則按照新的 vdom 中的節點從新生成一個節點,並把 prevChildren 中相同位置老節點刪除。按以上兩個狀態的 vdom tree,那麼遍歷完就會記錄下須要作兩步 dom 變動:新增一個 span 節點插入到第二個位置,刪除原來第二個位置上的 div。數組
再來看兩個例子:
例B:
遍歷結果:第二個節點新增一個span,刪除第二個div和第四個div。app
例C:
遍歷結果:第二個節點新增一個span,第四個節點新增一個div,刪除第二個div。dom
咱們看到對於例C來講其實最便利的方法就是把 span 插入到第二的位置上,而後其餘div只要作 attr 的更新而不須要再進行位置的增刪,若是 attr 都沒有變化,那麼後兩個 div 根本不須要變化。可是按例A裏面的算法,咱們須要進行好幾步的 dom 操做。這是爲算法減小時間複雜度,作了妥協。可是 react 對節點引入了 key 這個關鍵屬性幫助優化這種狀況。假設咱們給全部節點都添加了惟一的 key 屬性,以下面例D:
例D:
咱們在遍歷過程當中對所要記錄的東西進行優化,在某個位置碰到有 key 的節點咱們去 prevChildren 中找有沒有對應的節點,若是有,則咱們會比較當前節點在先後兩個 tree 中相對位置。若是相對位置沒有變化,則不須要作dom的增刪移,而只須要更新。若是位置不同則須要記錄把這個節點從老的位置移動到新的位置(具體算法須要藉助前一次dom變化的記錄這裏不詳述)。這樣從例C到例D的優化減小了 dom 節點的增刪。異步
可是 react 的這種算法的優化也帶來了一種極端的狀況:
例E:
遍歷結果:3次節點位置移動:2到1,1到2,0到3。
可是其實這裏只須要更新每一個節點的 attr,他們的位置根本不須要作變化。因此若是要給元素指定 key 最好避免元素的位置有太多太大的躍遷變化。
基本上 setState 以後到最終的 dom 變化的過程就是這麼結束了。
後記:梳理的比較簡單,不少細節我沒有精力做一一的總結,由於我本身看源碼看了很久,代碼中涉及到不少異步,事務等等干擾項,而後我本身又不想過多的藉助現有的資料-_-。當我快要把最後一點寫完的時候發現 pure render 專欄的做者陳屹出了一本《深刻React技術棧》裏面有至關詳細的源碼分析,因此我感受我這篇「白寫」了,貼出這本書就能夠了,不過陳屹的這本書是良心之做,必須安利下。