說在前面,著重梳理實際更新組件和 dom 部分的代碼,但是關于異步,transaction,批量合并新狀態等新細節只描述步驟。一來因為這些細節在讀源碼的時候只讀了部分,二來如果要把這些都寫出來要寫老長老長。
真實的 setState 的過程:
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 方法的說明:
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 方法:
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 中。
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。
例C:
遍歷結果:第二個節點新增一個span,第四個節點新增一個div,刪除第二個div。
我們看到對于例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技術棧》里面有相當詳細的源碼分析,所以我感覺我這篇“白寫”了,貼出這本書就可以了,不過陳屹的這本書是良心之作,必須安利下。