摘要:速度略有損失,但可讀性大大提高。與傳統對比傳統的算法通過循環遞歸每一個節點,進行對比,這樣的操作效率非常的低,復雜程度其中標識樹的節點總數。
原文鏈接:Nealyang PersonalBlog
由于源碼中diff算法摻雜了太多別的功能模塊,并且dom diff相對于之前的代碼實現來說還是有些麻煩的,尤其是列表對比的算法,所以這里我們多帶帶拿出來說他實現前言
眾所周知,React中最為人稱贊的就是Virtual DOM和 diff 算法的完美結合,讓我們可以不顧性能的“任性”更新界面,前面文章中我們有介紹道Virtual DOM,其實就是通過js來模擬dom的實現,然后通過對js obj的操作,最后渲染到頁面中,但是,如果當我們修改了一丟丟東西,就要渲染整個頁面的話,性能消耗還是非常大的,如何才能準確的修改該修改的地方就是我們diff算法的功能了。
其實所謂的diff算法大概就是當狀態發生改變的時候,重新構造一個新的Virtual DOM,然后根據與老的Virtual DOM對比,生成patches補丁,打到對應的需要修改的地方。
這里引用司徒正美的介紹
最開始經典的深度優先遍歷DFS算法,其復雜度為O(n^3),存在高昂的diff成本,然后是cito.js的橫空出世,它對今后所有虛擬DOM的算法都有重大影響。它采用兩端同時進行比較的算法,將diff速度拉高到幾個層次。緊隨其后的是kivi.js,在cito.js的基出提出兩項優化方案,使用key實現移動追蹤及基于key的編輯長度距離算法應用(算法復雜度 為O(n^2))。但這樣的diff算法太過復雜了,于是后來者snabbdom將kivi.js進行簡化,去掉編輯長度距離算法,調整兩端比較算法。速度略有損失,但可讀性大大提高。再之后,就是著名的vue2.0 把snabbdom整個庫整合掉了。與傳統diff對比
傳統的diff算法通過循環遞歸每一個節點,進行對比,這樣的操作效率非常的低,復雜程度O(n^3),其中n標識樹的節點總數。如果React僅僅是引入傳統的diff算法的話,其實性能也是非常差的。然而FB通過大膽的策略,滿足了大多數的性能最大化,將O(n^3)復雜度的問題成功的轉換成了O(n),并且后面對于同級節點移動,犧牲一定的DOM操作,算法的復雜度也才打到O(max(M,N))。
實現思路這里借用下網上的一張圖,感覺畫的非常贊~
大概解釋下:
額。。。其實上面也已近解釋了,當Virtual DOM發生變化的時,如上圖的第二個和第三個 p 的sonx被刪除了,這時候,我們就通過diff算法,計算出前后Virtual DOM的差異->補丁對象patches,然后根據這個patches對象中的信息來遍歷之前的老Virtual DOM樹,對其需要更新的地方進行更新,使其變成新VIrtual DOM。
diff 策略Web UI中節點跨級操作特別少,可以忽略不計
擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。(哪怕一樣的而我也認為不一樣 -> 大概率優化)
對于同一層級的一組子節點,他們可以通過唯一的key來區分,以方便后續的列表對比算法
基于如上,React分別對tree diff、Component diff 、element diff 進行了算法優化。
tree diff基于策略一,React的diff非常簡單明了:只會對同一層次的節點進行比較。這種非傳統的按深度遍歷搜索,這種通過大膽假設得到的改進方案,不僅符合實際場景的需要,而且大幅降低了算法實現復雜度,從O(n^3)提升至O(n)。
基于此,React官方并不推薦進行DOM節點的跨層級操作 ,倘若真的出現了,那就是非常消耗性能的remove和create的操作了。
我是真的不會畫圖Component diff
由于React是基于組件開發的,所以組件的dom diff其實也非常簡單,如果組件是同一類型,則進行tree diff比較。如果不是,則直接放入到patches中。即使是子組件結構類型都相同,只要父組件類型不同,都會被重新渲染。這也說明了為什么我們推薦使用shouldComponentUpdate來提高React性能。
大概的感覺是醬紫的
list diff對于節點的比較,其實只有三種操作,插入、移動和刪除。(這里最麻煩的是移動,后面會介紹實現)。當被diff節點處于同一層級時,通過三種節點操作新舊節點進行更新:插入,移動和刪除,同時提供給用戶設置key屬性的方式調整diff更新中默認的排序方式,在沒有key值的列表diff中,只能通過按順序進行每個元素的對比,更新,插入與刪除,在數據量較大的情況下,diff效率低下,如果能夠基于設置key標識盡心diff,就能夠快速識別新舊列表之間的變化內容,提升diff效率。
對于這三種理論知識可以參照知乎上不可思議的 react diff的介紹。
算法實現前方高清多碼預警
diff這里引入代碼處理我們先撇開list diff中的移動操作,先一步一步去實現
根據節點變更類型,我們定義如下幾種變化
const ATTRS = "ATTRS";//屬性改變 const TEXT = "TEXT";//文本改變 const REMOVE = "REMOVE";//移除操作 const REPLACE = "REPLACE";//替換操作 let Index = 0;
解釋下index,為了方便演示diff,我們暫時沒有想react源碼中給每一個Element添加唯一標識
var ReactElement = function(type, key, ref, self, source, owner, props) { var element = { // This tag allow us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE,//重點在這里 // Built-in properties that belong on the element type: type, key: key, ref: ref, props: props, // Record the component responsible for creating this element. _owner: owner, }; return element; }; ... "use strict"; // The Symbol used to tag the ReactElement type. If there is no native Symbol // nor polyfill, then a plain number is used for performance. var REACT_ELEMENT_TYPE = (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) || 0xeac7; module.exports = REACT_ELEMENT_TYPE;
我們遍歷每一個VDom,以index為索引。注意這里我們使用全局變量index,因為遍歷整個VDom,以index作為區分,所以必須用全局變量,當然,GitHub上有大神的實現方式為{index:0},哈~引用類型傳遞,換湯不換藥~
開始遍歷
export default function diff(oldTree, newTree) { let patches = {}; // 遞歸樹, 比較后的結果放到補丁包中 walk(oldTree, newTree, Index, patches) return patches; }
function walk(oldNode, newNode, index, patches) { let currentPatch = []; if(!newNode){ currentPatch.push({ type:REMOVE, index }); }else if(isString(oldNode) && isString(newNode)){ if(oldNode !== newNode){// 判斷是否為文本 currentPatch.push({ type:TEXT, text:newNode }); } }else if (oldNode.type === newNOde.type) { // 比較屬性是否有更改 let attrs = diffAttr(oldNode.porps, newNode.props); if (Object.keys(attrs).length > 0) { currentPatch.push({ type: ATTRS, attrs }); } // 比較兒子們 diffChildren(oldNode.children,newNode.children,patches); }else{ // 說明節點被替換 currentPatch.push({ type: REPLACE, newNode }); } currentPatch.length ? patches[index] = currentPatch : null; } function diffChildren(oldChildren,newChildren,patches) { oldChildren.forEach((child,ids)=>{ // index 每次傳遞給walk時, index應該是遞增的.所有的都基于同一個Index walk(child,newChildren[idx],++Index,patches); }) } function diffAttr(oldAttrs, newAttrs) { let patch = {}; // 判斷老屬性和新屬性的關系 for (let key in oldAttrs) { if (oldAttrs[key] !== newAttrs[key]) { patch[key] = newAttrs[key]; //有可能是undefined => 新節點中刪了該屬性 } } // 新節點新增了很多屬性 for (let key in newAttrs) { if (!oldAttrs.hasOwnProperty(key)) { patch[key] = newAttrs[key]; } } return patch; }
在diff過程中,我們需要去判斷文本標簽,需要在util中寫一個工具函數
function isString(node) { return Object.prototype.toString.call(node)==="[object String]"; }
實現思路非常簡單,手工流程圖了解下
通過diff后,最終我們會拿到新舊VDom的patches補丁,補丁的內容大致如下:
patches = { 1:{ type:"REMOVE", index:1 }, 3:{ type:"TEXT", newText:"hello Nealyang~", }, 6:{ type:"REPLACE", newNode:newNode } }
大致是這么個感覺,兩秒鐘體會下~
這里應該會有點詫異的是1 3 6...是什么鬼?
因為之前我們說過,diff采用的依舊是深度優先遍歷,及時你是改良后的升級產品,但是遍歷流程依舊是:
patches既然patches補丁已經拿到了,該如何使用呢,對,我們依舊是遍歷!
Element 調用render后,我們已經可以拿到一個通過VDom(代碼)解析后的真是Dom了,所以我們只需要將遍歷真實DOM,然后在指定位置修改對應的補丁上指定位置的更改就行了。
代碼如下:(自己實現的簡易版)
let allPaches = {}; let index = 0; //默認哪個需要補丁 export default function patch(dom, patches) { allPaches = patches; walk(dom); } function walk(dom) { let currentPatche = allPaches[index]; let childNodes = dom.childNodes; childNodes.forEach(element => walk(element)); if (currentPatche > 0) { doPatch(dom, currentPatche); } } function doPatch(node, patches) { patches.forEach(patch => { switch (patch.type) { case "ATTRS": setAttrs(patch.attrs)//別的文件方法 break; case "TEXT": node.textContent = patch.text; break; case "REPLACE": let newNode = patch.newNode instanceof Element ? render(patch.newNode) : document.createTextNode(patch.newNode); node.parentNode.replaceChild(newNode, node) break; case "REMOVE": node.parentNode.removeChild(node); break; } }) }
關于setAttrs其實功能都加都明白,這里給個簡單實例代碼,大家YY下
function setAttrs(dom, props) { const ALL_KEYS = Object.keys(props); ALL_KEYS.forEach(k =>{ const v = props[k]; // className if(k === "className"){ dom.setAttribute("class",v); return; } if(k == "style") { if(typeof v == "string") { dom.style.cssText = v } if(typeof v == "object") { for (let i in v) { dom.style[i] = v[i] } } return } if(k[0] == "o" && k[1] == "n") { const capture = (k.indexOf("Capture") != -1) dom.addEventListener(k.substring(2).toLowerCase(),v,capture) return } dom.setAttribute(k, v) }) }
如上,其實我們已經實現了DOM diff了,但是存在一個問題.
如下圖,老集合中包含節點:A、B、C、D,更新后的新集合中包含節點:B、A、D、C,此時新老集合進行 diff 差異化對比,發現 B != A,則創建并插入 B 至新集合,刪除老集合 A;以此類推,創建并插入 A、D 和 C,刪除 B、C 和 D。
針對這一現象,React 提出優化策略:允許開發者對同一層級的同組子節點,添加唯一 key 進行區分,雖然只是小小的改動,性能上卻發生了翻天覆地的變化!
具體介紹可以參照 https://zhuanlan.zhihu.com/p/20346379
這里我們放到代碼實現上:
/** * Diff two list in O(N). * @param {Array} oldList - Original List * @param {Array} newList - List After certain insertions, removes, or moves * @return {Object} - {moves:} * - moves is a list of actions that telling how to remove and insert */ function diff (oldList, newList, key) { var oldMap = makeKeyIndexAndFree(oldList, key) var newMap = makeKeyIndexAndFree(newList, key) var newFree = newMap.free var oldKeyIndex = oldMap.keyIndex var newKeyIndex = newMap.keyIndex var moves = [] // a simulate list to manipulate var children = [] var i = 0 var item var itemKey var freeIndex = 0 // first pass to check item in old list: if it"s removed or not // 遍歷舊的集合 while (i < oldList.length) { item = oldList[i] itemKey = getItemKey(item, key)//itemKey a // 是否可以取到 if (itemKey) { // 判斷新集合中是否有這個屬性,如果沒有則push null if (!newKeyIndex.hasOwnProperty(itemKey)) { children.push(null) } else { // 如果有 去除在新列表中的位置 var newItemIndex = newKeyIndex[itemKey] children.push(newList[newItemIndex]) } } else { var freeItem = newFree[freeIndex++] children.push(freeItem || null) } i++ } // children [{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}] var simulateList = children.slice(0)//[{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}] // remove items no longer exist i = 0 while (i < simulateList.length) { if (simulateList[i] === null) { remove(i) removeSimulate(i) } else { i++ } } // i is cursor pointing to a item in new list // j is cursor pointing to a item in simulateList var j = i = 0 while (i < newList.length) { item = newList[i] itemKey = getItemKey(item, key)//c var simulateItem = simulateList[j] //{id:"a"} var simulateItemKey = getItemKey(simulateItem, key)//a if (simulateItem) { if (itemKey === simulateItemKey) { j++ } else { // 新增項,直接插入 if (!oldKeyIndex.hasOwnProperty(itemKey)) { insert(i, item) } else { // if remove current simulateItem make item in right place // then just remove it var nextItemKey = getItemKey(simulateList[j + 1], key) if (nextItemKey === itemKey) { remove(i) removeSimulate(j) j++ // after removing, current j is right, just jump to next one } else { // else insert item insert(i, item) } } } } else { insert(i, item) } i++ } //if j is not remove to the end, remove all the rest item var k = simulateList.length - j while (j++ < simulateList.length) { k-- remove(k + i) } // 記錄舊的列表中移除項 {index:3,type:0} function remove (index) { var move = {index: index, type: 0} moves.push(move) } function insert (index, item) { var move = {index: index, item: item, type: 1} moves.push(move) } // 刪除simulateList中null function removeSimulate (index) { simulateList.splice(index, 1) } return { moves: moves, children: children } } /** * Convert list to key-item keyIndex object. * 將列表轉換為 key-item 的鍵值對象 * [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] -> [a:0,b:1,c:2...] * @param {Array} list * @param {String|Function} key */ function makeKeyIndexAndFree (list, key) { var keyIndex = {} var free = [] for (var i = 0, len = list.length; i < len; i++) { var item = list[i] var itemKey = getItemKey(item, key) if (itemKey) { keyIndex[itemKey] = i } else { free.push(item) } } return { keyIndex: keyIndex, free: free } } // 獲取置頂key的value function getItemKey (item, key) { if (!item || !key) return void 666 return typeof key === "string" ? item[key] : key(item) } exports.makeKeyIndexAndFree = makeKeyIndexAndFree exports.diffList = diff
代碼參照:list-diff 具體的注釋都已經加上。
使用如下:
import {diffList as diff} from "./lib/diffList"; var oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] var newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}] var moves = diff(oldList, newList, "id") // type 0 表示移除, type 1 表示插入 // moves: [ // {index: 3, type: 0}, // {index: 0, type: 1, item: {id: "c"}}, // {index: 3, type: 0}, // {index: 4, type: 1, item: {id: "f"}} // ] console.log(moves) moves.moves.forEach(function(move) { if (move.type === 0) { oldList.splice(move.index, 1) // type 0 is removing } else { oldList.splice(move.index, 0, move.item) // type 1 is inserting } }) // now `oldList` is equal to `newList` // [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}] console.log(oldList)
這里我最困惑的地方時,實現diff都是index為索引,深度優先遍歷,如果存在這種移動操作的話,那么之前我補丁patches里記錄的index不就沒有意義了么??
在 后來在開源的simple-virtual-dom中找到了index作為索引和標識去實現diff的答案。
第一點:在createElement的時候,去記錄每一元素children的count數量
function Element(tagName, props, children) { if (!(this instanceof Element)) { if (!_.isArray(children) && children != null) { children = _.slice(arguments, 2).filter(_.truthy) } return new Element(tagName, props, children) } if (_.isArray(props)) { children = props props = {} } this.tagName = tagName this.props = props || {} this.children = children || [] this.key = props ? props.key : void 666 var count = 0 _.each(this.children, function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = "" + child } count++ }) this.count = count }
第二點,在diff算法中,遇到移動的時候,我們需要及時更新我們全局變量index,核心代碼`(leftNode && leftNode.count) ?
currentNodeIndex + leftNode.count + 1 :
currentNodeIndex + 1`。完整代碼如下:
function diffChildren(oldChildren, newChildren, index, patches, currentPatch) { var diffs = diffList(oldChildren, newChildren, "key") newChildren = diffs.children if (diffs.moves.length) { var reorderPatch = { type: patch.REORDER, moves: diffs.moves } currentPatch.push(reorderPatch) } var leftNode = null var currentNodeIndex = index _.each(oldChildren, function (child, i) { var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) leftNode = child }) }
話說,這里困擾了我好久好久。。。。
回到開頭
var REACT_ELEMENT_TYPE = (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) || 0xeac7;
也就說明了這段代碼的必要性。
0.3中diff的實現最后我們在看下0.3中diff的實現:
updateMultiChild: function(nextChildren, transaction) { if (!nextChildren && !this._renderedChildren) { return; } else if (nextChildren && !this._renderedChildren) { this._renderedChildren = {}; // lazily allocate backing store with nothing } else if (!nextChildren && this._renderedChildren) { nextChildren = {}; } var rootDomIdDot = this._rootNodeID + "."; var markupBuffer = null; // Accumulate adjacent new children markup. var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer var loopDomIndex = 0; // Index of loop through new children. var curChildrenDOMIndex = 0; // See (Comment 1) for (var name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) {continue;} // 獲取當前節點與要渲染的節點 var curChild = this._renderedChildren[name]; var nextChild = nextChildren[name]; // 是否兩個節點都存在,且類型相同 if (shouldManageExisting(curChild, nextChild)) { // 如果有插入標示,之后又循環到了不需要插入的節點,則直接插入,并把插入標示制空 if (markupBuffer) { this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert); markupBuffer = null; } numPendingInsert = 0; // 如果找到當前要渲染的節點序號比最大序號小,則移動節點 /* * 在0.3中,沒有根據key做diff,而是通過Object中的key作為索引 * 比如{a,b,c}替換成{c,b,c} * b._domIndex = 1挪到loopDomIndex = 1的位置,就是原地不動 a._domIndex = 0挪到loopDomIndex = 2的位置,也就是和c換位 */ if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2) this.enqueueMove(curChild._domIndex, loopDomIndex); } curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex); // 遞歸更新子節點Props,調用子節點dom-diff... !nextChild.props.isStatic && curChild.receiveProps(nextChild.props, transaction); curChild._domIndex = loopDomIndex; } else { // 當前存在,執行刪除 if (curChild) { // !shouldUpdate && curChild => delete this.enqueueUnmountChildByName(name, curChild); curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex); } // 當前不存在,下個節點存在, 執行插入,渲染下個節點 if (nextChild) { // !shouldUpdate && nextChild => insert this._renderedChildren[name] = nextChild; // 渲染下個節點 var nextMarkup = nextChild.mountComponent(rootDomIdDot + name, transaction); markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup; numPendingInsert++; nextChild._domIndex = loopDomIndex; } } loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex; } // 執行插入操作,插入位置計算方式如下: // 要渲染的節點位置-要插入的節點個數:比如當前要渲染的節點index=3,當前節點只有一個,也就是index=1。 // 如1渲染成123// 那么從2開始就開始加入buffer,最終buffer內容為23// 那么要插入的位置為 3 - 1 = 2。我們以1為1,就是把buffer插入2的位置,也就是1后面 if (markupBuffer) { this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert); } // 循環老節點 for (var childName in this._renderedChildren) { if (!this._renderedChildren.hasOwnProperty(childName)) { continue; } var child = this._renderedChildren[childName]; // 當前節點存在,下個節點不存在,刪除 if (child && !nextChildren[childName]) { this.enqueueUnmountChildByName(childName, child); } } // 一次提交所有操作 this.processChildDOMOperationsQueue(); }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97504.html
摘要:因為版本將真正廢棄這三生命周期到目前為止,的渲染機制遵循同步渲染首次渲染,更新時更新時卸載時期間每個周期函數各司其職,輸入輸出都是可預測,一路下來很順暢。通過進一步觀察可以發現,預廢棄的三個生命周期函數都發生在虛擬的構建期間,也就是之前。 showImg(https://segmentfault.com/img/bVbweoj?w=559&h=300); 背景 前段時間準備前端招聘事項...
摘要:所以只針對同層級節點做比較,將復雜度的問題轉換成復雜度的問題。 React系列 React系列 --- 簡單模擬語法(一)React系列 --- Jsx, 合成事件與Refs(二)React系列 --- virtualdom diff算法實現分析(三)React系列 --- 從Mixin到HOC再到HOOKS(四)React系列 --- createElement, ReactElem...
摘要:目前,前端領域中勢頭正盛,使用者眾多卻少有能夠深入剖析內部實現機制和原理。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。 目前,前端領域中 React 勢頭正盛,使用者眾多卻少有能夠深入剖析內部實現機制和原理。本系列文章希望通過剖析 React 源碼,理解其內部的實現原理,知其然更要知其所以然。 React diff 作為 Virtual DOM 的加速...
摘要:在上面我們已經知道瀏覽器是一幀一幀執行的,在兩個執行幀之間,主線程通常會有一小段空閑時間,可以在這個空閑期調用空閑期回調,執行一些任務。另外由于這些堆棧是可以自己控制的,所以可以加入并發或者錯誤邊界等功能。 文章首發于個人博客 前言 2016 年都已經透露出來的概念,這都 9102 年了,我才開始寫 Fiber 的文章,表示慚愧呀。不過現在好的是關于 Fiber 的資料已經很豐富了,...
摘要:正式開始系統地學習前端已經三個多月了,感覺前端知識體系龐雜但是又非常有趣。更新一個節點需要做的事情有兩件,更新頂層標簽的屬性,更新這個標簽包裹的子節點。 正式開始系統地學習前端已經三個多月了,感覺前端知識體系龐雜但是又非常有趣。前端演進到現在對開發人員的代碼功底要求已經越來越高,幾年前的前端開發還是大量操作DOM,直接與用戶交互,而React、Vue等MVVM框架的出現,則幫助開發者從...
閱讀 1588·2021-09-24 10:38
閱讀 1521·2021-09-22 15:15
閱讀 3070·2021-09-09 09:33
閱讀 913·2019-08-30 11:08
閱讀 648·2019-08-30 10:52
閱讀 1261·2019-08-30 10:52
閱讀 2356·2019-08-28 18:01
閱讀 530·2019-08-28 17:55