歡迎關注個人公衆號睿Talk
,獲取我最新的文章:javascript
React 是一個十分龐大的庫,因爲要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,致使其代碼抽象化程度很高,嵌套層級很是深。閱讀 React 源碼是一個很是艱辛的過程,在學習過程當中給我幫助最大的就是這個系列文章。做者對代碼的調用關係梳理得很是清楚,並且還有配圖幫助理解,很是值得一讀。站在巨人的肩膀之上,我嘗試再加入本身的理解,但願對有志於學習 React 源碼的讀者帶來一點啓發。前端
本系列文章基於 React 15.4.2 ,如下是本系列其它文章的傳送門:
React 源碼深度解讀(一):首次 DOM 元素渲染 - Part 1
React 源碼深度解讀(二):首次 DOM 元素渲染 - Part 2
React 源碼深度解讀(三):首次 DOM 元素渲染 - Part 3
React 源碼深度解讀(四):首次自定義組件渲染 - Part 1
React 源碼深度解讀(五):首次自定義組件渲染 - Part 2
React 源碼深度解讀(六):依賴注入
React 源碼深度解讀(七):事務 - Part 1
React 源碼深度解讀(八):事務 - Part 2
React 源碼深度解讀(九):單個元素更新
React 源碼深度解讀(十):Diff 算法詳解java
React 在比較新舊 2 棵虛擬 DOM 樹的時候,會同時考慮兩點:react
若是隻考慮第一點,算法的時間複雜度要達到 O(n3) 的級別。也就是說對於一個有 1000 個節點的樹,要進行 10 億次的比較,這對於前端應用來講是徹底不可接受的。所以,React 選用了啓發式的算法,將時間複雜度控制在 O(n) 的級別。這個算法基於如下 2 個假設:git
另外,React 只會對同一層的節點做比較,不會跨層級比較,如圖所示:github
Diff 使用的是深度優先算法,當遇到下圖這樣的狀況:算法
最高效的算法應該是直接將 A 子樹移動到 D 節點,但這樣就涉及到跨層級比較,時間複雜度會陡然上升。React 的作法比較簡單,它會先刪除整個 A 子樹,而後再從新建立一遍。結合到實際的使用場景,改變一個組件的從屬關係的狀況也是不多的。segmentfault
一樣道理,當 D 節點改成 G 節點時,整棵 D 子樹也會被刪掉,E、F 節點會從新建立。數組
對於列表的 Diff,節點的 key 有助於節點的重用:服務器
如上圖所示,當沒有 key 的時候,若是中間插入一個新節點,Diff 過程當中從第三個節點開始的節點都是刪除舊節點,建立新節點。當有 key 的時候,除了第三個節點是新建立外,第四和第五個節點都是經過移動實現的。
在瞭解了整體的 Diff 策略後,咱們來近距離的審視源碼。先更新示例代碼,注意 li 元素沒有使用 key :
class App extends Component { constructor(props) { super(props); this.state = { data : ['one', 'two'], }; this.timer = setInterval( () => this.tick(), 5000 ); } tick() { this.setState({ data: ['new', 'one', 'two'], }); } render() { return ( <ul> { this.state.data.map(function(val, i) { return <li>{ val }</li>; }) } </ul> ); } } export default App;
元素變化以下圖所示,5 秒後會新增一個 new 元素。
咱們跳過前面的邏輯,直接來看 Diff 的代碼
_updateRenderedComponent: function (transaction, context) { var prevComponentInstance = this._renderedComponent; // 以前的 Virtual DOM var prevRenderedElement = prevComponentInstance._currentElement; // 最新的 Virtual DOM var nextRenderedElement = this._renderValidatedComponent(); var debugID = 0; if (__DEV__) { debugID = this._debugID; } if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { ReactReconciler.receiveComponent( prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context) ); } else { ... } }, // shouldUpdateReactComponent.js 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 ); } }
_updateRenderedComponent
方法位於 ReactCompositeComponent 內。它先獲取新、舊 2 個 Virtual DOM,而後經過shouldUpdateReactComponent
判斷節點類型是否相同。在咱們的例子裏,跟節點都是 ul 元素,所以跳過中間的層級後,會走到 ReactDOMComponent 的 updateComponent 方法。他會更新屬性和子元素,更新屬性部分上一篇文章已經講清楚了,下面看看更新子元素部分。最終會調用 ReactMultiChild 的 _updateChildren :
_updateChildren: function (nextNestedChildrenElements, transaction, context) { ... var nextChildren = this._reconcilerUpdateChildren( prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context ); ... for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { updates = enqueue( updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex) ); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. } // The child must be instantiated before it's mounted. updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context ) ); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // Remove children that are no longer present. for (name in removedNodes) { if (removedNodes.hasOwnProperty(name)) { updates = enqueue( updates, this._unmountChild(prevChildren[name], removedNodes[name]) ); } } if (updates) { processQueue(this, updates); } this._renderedChildren = nextChildren; }, _reconcilerUpdateChildren: function ( prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context ) { var nextChildren; var selfDebugID = 0; nextChildren = flattenChildren(nextNestedChildrenElements, selfDebugID); ReactChildReconciler.updateChildren( prevChildren, nextChildren, mountImages, removedNodes, transaction, this, this._hostContainerInfo, context, selfDebugID ); return nextChildren; },
在開始 Diff li 元素以前,它會先調用_reconcilerUpdateChildren
去更新 li 元素的子元素,也就是文本。在函數中調用了flattenChildren
方法,將數組轉換成對象:
function flattenSingleChildIntoContext( traverseContext: mixed, child: ReactElement < any > , name: string, selfDebugID ? : number, ): void { // We found a component instance. if (traverseContext && typeof traverseContext === 'object') { const result = traverseContext; const keyUnique = (result[name] === undefined); if (keyUnique && child != null) { result[name] = child; } } } function flattenChildren( children: ReactElement < any > , selfDebugID ? : number, ): ? { [name: string]: ReactElement < any > } { if (children == null) { return children; } var result = {}; traverseAllChildren(children, flattenSingleChildIntoContext, result); return result; } // traverseAllChildren.js function traverseAllChildren(children, callback, traverseContext) { if (children == null) { return 0; } return traverseAllChildrenImpl(children, '', callback, traverseContext); } function traverseAllChildrenImpl( children, nameSoFar, callback, traverseContext ) { var type = typeof children; if (type === 'undefined' || type === 'boolean') { // All of the above are perceived as null. children = null; } if (children === null || type === 'string' || type === 'number' || // The following is inlined from ReactElement. This means we can optimize // some checks. React Fiber also inlines this logic for similar purposes. (type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE)) { callback( traverseContext, children, // If it's the only child, treat the name as if it was wrapped in an array // so that it's consistent if the number of children grows. nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar ); return 1; } var child; var nextName; var subtreeCount = 0; // Count of children found in the current subtree. var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR; if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { child = children[i]; nextName = nextNamePrefix + getComponentKey(child, i); subtreeCount += traverseAllChildrenImpl( child, nextName, callback, traverseContext ); } } ... return subtreeCount; } function getComponentKey(component, index) { // Do some typechecking here since we call this blindly. We want to ensure // that we don't block potential future ES APIs. if (component && typeof component === 'object' && component.key != null) { // Explicit key return KeyEscapeUtils.escape(component.key); } // Implicit key determined by the index in the set return index.toString(36); }
flattenSingleChildIntoContext
做爲flattenChildren
的回調函數,會做用在每個數組元素上,構造一個對象(result)。對象的 key 是經過getComponentKey
這個方法生成的,能夠看到若是沒有定義 key 屬性,則默認會用數組的 index 做爲 key 。最終構造出來的對象是這個樣子的:
而後就會調用ReactChildReconciler.updateChildren
方法,去更新 li 的文本和建立新的 li 元素。
updateChildren: function ( prevChildren, nextChildren, mountImages, removedNodes, transaction, hostParent, hostContainerInfo, context, selfDebugID // 0 in production and for roots ) { if (!nextChildren && !prevChildren) { return; } var name; var prevChild; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } prevChild = prevChildren && prevChildren[name]; var prevElement = prevChild && prevChild._currentElement; var nextElement = nextChildren[name]; if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) { ReactReconciler.receiveComponent( prevChild, nextElement, transaction, context ); nextChildren[name] = prevChild; } else { if (prevChild) { removedNodes[name] = ReactReconciler.getHostNode( prevChild); ReactReconciler.unmountComponent(prevChild, false); } // The child must be instantiated before it's mounted. var nextChildInstance = instantiateReactComponent( nextElement, true); nextChildren[name] = nextChildInstance; // Creating mount image now ensures refs are resolved in right order // (see https://github.com/facebook/react/pull/7101 for explanation). var nextChildMountImage = ReactReconciler.mountComponent( nextChildInstance, transaction, hostParent, hostContainerInfo, context, selfDebugID ); mountImages.push(nextChildMountImage); } } // Unmount children that are no longer present. for (name in prevChildren) { if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) { prevChild = prevChildren[name]; removedNodes[name] = ReactReconciler.getHostNode( prevChild); ReactReconciler.unmountComponent(prevChild, false); } } },
在更新 li 前,會根據 key 去取上一次 render 對應的元素prevChild = prevChildren && prevChildren[name]
。以第 0 個元素爲例,prevElement 爲 ReactElement[3]( key 爲‘.0’),而 nextElement 爲 ReactElement[2],所以會調用ReactReconciler.receiveComponent
來更新文本元素,過程與上一篇文章同樣。
當遍歷到最後一個 li ,因爲沒有 prevChild,會建立一個新的實例。
再回到 ReactMultiChild 的 _updateChildren 方法,這時nextChildren
已經建立好,開始遍歷:
_updateChildren: function (nextNestedChildrenElements, transaction, context) { ... var nextChildren = this._reconcilerUpdateChildren( prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context ); ... for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { ------------------------------------ 1) updates = enqueue( updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex) ); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { ------------------------------------ 2) if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. } // The child must be instantiated before it's mounted. updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context ) ); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // Remove children that are no longer present. for (name in removedNodes) { if (removedNodes.hasOwnProperty(name)) { updates = enqueue( updates, this._unmountChild(prevChildren[name], removedNodes[name]) ); } } if (updates) { processQueue(this, updates); } this._renderedChildren = nextChildren; },
前面 2 個 li 元素會走到分支 1),第三個元素會到分支 2),建立新的 li 元素,過程與上一篇的相似。
例子改一下,加入 key:
class App extends Component { constructor(props) { super(props); this.state = { data: ['one', 'two'] }; this.timer = setTimeout(() => this.tick(), 5000); } tick() { this.setState({ data: ['new', 'one', 'two'] }); } render() { return ( <ul> { this.state.data.map(function (val, i) { return <li key={val}>{ val }</li>; }) } </ul> ); } }
流程與無 key 相似,再也不贅述。flattenChildren
後的對象是這個樣子的:
因爲使用了 key ,ReactChildReconciler.updateChildren
再也不須要更新 text 了,只須要建立一個新的實例。
而後到 ReactMultiChild 的 _updateChildren :
_updateChildren: function (nextNestedChildrenElements, transaction, context) { ... for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { ------------------------------------ 1) updates = enqueue( updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex) ); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { ------------------------------------ 2) if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. } // The child must be instantiated before it's mounted. updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context ) ); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // Remove children that are no longer present. for (name in removedNodes) { if (removedNodes.hasOwnProperty(name)) { updates = enqueue( updates, this._unmountChild(prevChildren[name], removedNodes[name]) ); } } if (updates) { processQueue(this, updates); } this._renderedChildren = nextChildren; },
匹配第一個元素的時候,會到分支 2),後面 2 個元素都是走分支 1)。
有 key 跟沒 key 相比,減小了 2 次文本元素的更新,提升了效率。
再來考慮更復雜的狀況:
假設要作上圖的變化,再來分析下源碼:
_updateChildren: function (nextNestedChildrenElements, transaction, context) { ... var nextIndex = 0; var lastIndex = 0; var nextMountIndex = 0; var lastPlacedNode = null; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { ------------------------------------ 1) updates = enqueue( updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex) ); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { ------------------------------------ 2) if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. } // The child must be instantiated before it's mounted. updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context ) ); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } ... }, moveChild: function (child, afterNode, toIndex, lastIndex) { // If the index of `child` is less than `lastIndex`, then it needs to // be moved. Otherwise, we do not need to move it because a child will be // inserted or moved before `child`. if (child._mountIndex < lastIndex) { return makeMove(child, afterNode, toIndex); } },
這裏要先搞清楚 3 個 index:
下面咱們來走一遍流程:
_mountIndex < lastIndex
,所以 B 不須要移動。lastIndex 更新爲 _mountIndex 和 lastIndex 中較大的:1 。nextIndex 自增:1;_mountIndex < lastIndex
,所以將 A 移動到 lastPlacedNode (B)的後面 。lastIndex 更新爲 _mountIndex 和 lastIndex 中較大的:1 。nextIndex 自增:2;_mountIndex < lastIndex
,所以 D 不須要移動。lastIndex 更新爲 _mountIndex 和 lastIndex 中較大的:3 。nextIndex 自增:3;_mountIndex < lastIndex
,所以將 C 移動到 lastPlacedNode (D)的後面。循環結束。觀察整個過程,移動的原則是將原來的元素往右邊移,經過 lastIndex 來控制。在 lastIndex 左邊的,就往 lastIndex 的右邊移動;在 lastIndex 左邊的,不須要動。
本文詳細講解了 Diff 過程當中 key 的做用以及複用節點的移動原則,並結合源碼作了深刻的討論。到此爲止,整個 React 源碼解讀系列先告一段落了,後會有期。