摘要:的做法比較簡單,它會先刪除整個子樹,然后再重新創建一遍。同樣道理,當節點改為節點時,整棵子樹也會被刪掉,節點會重新創建。更新為和中較大的。到此為止,整個源碼解讀系列先告一段落了,后會有期。
歡迎關注我的公眾號睿Talk,獲取我最新的文章:
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 算法詳解
React 在比較新舊 2 棵虛擬 DOM 樹的時候,會同時考慮兩點:
盡量少的創建 / 刪除節點,多使用移動節點的方式
比較次數要盡量少,算法要足夠的快
如果只考慮第一點,算法的時間復雜度要達到 O(n3) 的級別。也就是說對于一個有 1000 個節點的樹,要進行 10 億次的比較,這對于前端應用來說是完全不可接受的。因此,React 選用了啟發式的算法,將時間復雜度控制在 O(n) 的級別。這個算法基于以下 2 個假設:
如果 2 個節點的類型不一樣,以這 2 個節點為根結點的樹會完全不同
對于多次 render 中結構保持不變的節點,開發者會用一個 key 屬性標識出來,以便重用
另外,React 只會對同一層的節點作比較,不會跨層級比較,如圖所示:
Diff 使用的是深度優先算法,當遇到下圖這樣的情況:
最高效的算法應該是直接將 A 子樹移動到 D 節點,但這樣就涉及到跨層級比較,時間復雜度會陡然上升。React 的做法比較簡單,它會先刪除整個 A 子樹,然后再重新創建一遍。結合到實際的使用場景,改變一個組件的從屬關系的情況也是很少的。
同樣道理,當 D 節點改為 G 節點時,整棵 D 子樹也會被刪掉,E、F 節點會重新創建。
對于列表的 Diff,節點的 key 有助于節點的重用:
如上圖所示,當沒有 key 的時候,如果中間插入一個新節點,Diff 過程中從第三個節點開始的節點都是刪除舊節點,創建新節點。當有 key 的時候,除了第三個節點是新創建外,第四和第五個節點都是通過移動實現的。
三、無 Key Diff在了解了總體的 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 (
元素變化如下圖所示,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 Diff例子改一下,加入 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 (
流程與無 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 次文本元素的更新,提高了效率。
五、深挖 Key Diff再來考慮更復雜的情況:
假設要做上圖的變化,再來分析下源碼:
_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:
nextIndex:遍歷 nextChildren 時候的 index,每遍歷一個元素加 1
lastIndex:上一次從 prevChildren 中取出來元素時,這個元素在 prevChildren 中的 index
_mountIndex:元素在數組中的位置
下面我們來走一遍流程:
nextChildren 的第一個元素是 B ,在 prevChildren 中的位置是 1(_mountIndex),nextIndex 和 lastIndex 都是 0。節點移動的前提是_mountIndex < lastIndex,因此 B 不需要移動。lastIndex 更新為 _mountIndex 和 lastIndex 中較大的:1 。nextIndex 自增:1;
nextChildren 的第二個元素是 A ,在 prevChildren 中的位置是 0(_mountIndex),nextIndex 和 lastIndex 都是 1。這時_mountIndex < lastIndex,因此將 A 移動到 lastPlacedNode (B)的后面 。lastIndex 更新為 _mountIndex 和 lastIndex 中較大的:1 。nextIndex 自增:2;
nextChildren 的第三個元素是 D ,中 prevChildren 中的位置是 3(_mountIndex),nextIndex 是 2 ,lastIndex 是 1。這時不滿足_mountIndex < lastIndex,因此 D 不需要移動。lastIndex 更新為 _mountIndex 和 lastIndex 中較大的:3 。nextIndex 自增:3;
nextChildren 的第四個元素是 C ,中 prevChildren 中的位置是 2(_mountIndex),nextIndex 是 3 ,lastIndex 是 3。這時_mountIndex < lastIndex,因此將 C 移動到 lastPlacedNode (D)的后面。循環結束。
觀察整個過程,移動的原則是將原來的元素往右邊移,通過 lastIndex 來控制。在 lastIndex 左邊的,就往 lastIndex 的右邊移動;在 lastIndex 左邊的,不需要動。
六、總結本文詳細講解了 Diff 過程中 key 的作用以及復用節點的移動原則,并結合源碼做了深入的討論。到此為止,整個 React 源碼解讀系列先告一段落了,后會有期。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108606.html
摘要:在學習源碼的過程中,給我幫助最大的就是這個系列文章,于是決定基于這個系列文章談一下自己的理解。到此為止,首次渲染就完成啦總結從啟動到元素渲染到頁面,并不像看起來這么簡單,中間經歷了復雜的層級調用。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過...
摘要:依賴注入和控制反轉,這兩個詞經常一起出現。一句話表述他們之間的關系依賴注入是控制反轉的一種實現方式。而兩者有大量的代碼都是可以共享的,這就是依賴注入的使用場景了。下一步就是創建具體的依賴內容,然后注入到需要的地方這里的等于這個對象。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級...
摘要:本篇開始介紹自定義組件是如何渲染的。組件將自定義組件命名為,結構如下經過編譯后,生成如下代碼構建頂層包裝組件跟普通元素渲染一樣,第一步先會執行創建為的。調用順序已在代碼中注釋。先看圖,這部分內容將在下回分解 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非...
摘要:前言是一個十分龐大的庫,由于要同時考慮和,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過程。在學習源碼的過程中,給我幫助最大的就是這個系列文章,于是決定基于這個系列文章談一下自己的理解。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務器渲染等,導致其代碼抽象化程度很高,嵌套層級非常...
摘要:本文將要講解的調用棧是下面這個樣子的平臺無關相關如果看源碼,我們會留意到很多相關的代碼,我們暫時先將其忽略,會在后續的文章中進行講解。現在我們來看一下各實例間的關系目前為止的調用棧平臺無關相關下一篇講解三總結本文講解了轉化為的過程。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 R...
閱讀 3518·2021-11-17 17:01
閱讀 3927·2021-11-08 13:12
閱讀 2482·2021-10-08 10:04
閱讀 695·2021-09-29 09:35
閱讀 1425·2021-09-26 10:12
閱讀 2040·2021-09-07 09:58
閱讀 1958·2019-08-30 15:55
閱讀 2139·2019-08-30 13:14