摘要:最后里面沒有第四個元素了,才會把蘋果從移除。四總結本文基于上一個版本的代碼,加入了對唯一標識的支持,很好的提高了更新數組元素的效率。
歡迎關注我的公眾號睿Talk,獲取我最新的文章:
目前最流行的兩大前端框架,React和Vue,都不約而同的借助Virtual DOM技術提高頁面的渲染效率。那么,什么是Virtual DOM?它是通過什么方式去提升頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的創建過程,并實現一個簡單的Diff算法來更新頁面。本文的內容脫離于任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一律用VD表示。
這是VD系列文章的第四篇,以下是本系列其它文章的傳送門:
你不知道的Virtual DOM(一):Virtual Dom介紹
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新優化
你不知道的Virtual DOM(四):key的作用
你不知道的Virtual DOM(五):自定義組件
你不知道的Virtual DOM(六):事件處理&異步更新
今天,我們繼續在之前項目的基礎上進行優化。用過React或者Vue的朋友都知道在渲染數組元素的時候,編譯器會提醒加上key這個屬性,那么key是用來做什么的呢?
二、key的作用在渲染數組元素時,它們一般都有相同的結構,只是內容有些不同而已,比如:
可以把這個例子想象成一個購物車。此時如果想往購物車里面添加一件商品,性能不會有任何問題,因為只是簡單的在ul的末尾追加元素,前面的元素都不需要更新:
但是,如果我要刪除第一個元素,根據VD的比較邏輯,后面的元素全部都要進行更新的操作。dom結構簡單還好說,如果是一個復雜的結構,那頁面渲染的性能將會受到很大的影響。
有什么方式可以降低這種性能的損耗呢?
最直觀的方法肯定是直接刪除第一個元素然后其它元素保持不變了。但程序沒有這么智能,可以像我們一樣一眼就看出變化。程序能做到的是盡量少的修改元素,通過移動元素而不是修改元素來達到更新的目的。為了告訴程序要怎么移動元素,我們必須給每個元素加上一個唯一標識,也就是key。
當把蘋果刪掉的時候,VD里面第一個元素是香蕉,而dom里面第一個元素是蘋果。當元素有key屬性的時候,框架就會嘗試根據這個key去找對應的元素,找到了就將這個元素移動到第一個位置,循環往復。最后VD里面沒有第四個元素了,才會把蘋果從dom移除。
三、代碼實現在上一個版本代碼的基礎上,主要的改動點是diffChildren這個函數。原來的實現很簡單,遞歸的調用diff就可以了:
function diffChildren(newVDom, parent) { // 獲取子元素最大長度 const childLength = Math.max(parent.childNodes.length, newVDom.children.length); // 遍歷并diff子元素 for (let i = 0; i < childLength; i++) { diff(newVDom.children[i], parent, i); } }
現在,我們要對這個函數進行一個大改造,讓他支持key的查找:
function diffChildren(newVDom, parent) { // 有key的子元素 const nodesWithKey = {}; let nodesWithKeyCount = 0; // 沒key的子元素 const nodesWithoutKey = []; let nodesWithoutKeyCount = 0; const childNodes = parent.childNodes, nodeLength = childNodes.length; const vChildren = newVDom.children, vLength = vChildren.length; // 用于優化沒key子元素的數組遍歷 let min = 0; // 將子元素分成有key和沒key兩組 for (let i = 0; i < nodeLength; i++) { const child = childNodes[i], props = child[ATTR_KEY]; if (props !== undefined && props.key !== undefined) { nodesWithKey[props.key] = child; nodesWithKeyCount++; } else { nodesWithoutKey[nodesWithoutKeyCount++] = child; } } // 遍歷vdom的所有子元素 for (let i = 0; i < vLength; i++) { const vChild = vChildren[i], vProps = vChild.props; let dom; vKey = vProps!== undefined ? vProps.key : undefined; // 根據key來查找對應元素 if (vKey !== undefined) { if (nodesWithKeyCount && nodesWithKey[vKey] !== undefined) { dom = nodesWithKey[vKey]; nodesWithKey[vKey] = undefined; nodesWithKeyCount--; } } // 如果沒有key字段,則找一個類型相同的元素出來做比較 else if (min < nodesWithoutKeyCount) { for (let j = 0; j < nodesWithoutKeyCount; j++) { const node = nodesWithoutKey[j]; if (node !== undefined && isSameType(node, vChild)) { dom = node; nodesWithoutKey[j] = undefined; if (j === min) min++; if (j === nodesWithoutKeyCount - 1) nodesWithoutKeyCount--; break; } } } // diff返回是否更新元素 const isUpdate = diff(dom, vChild, parent); // 如果是更新元素,且不是同一個dom元素,則移動到原先的dom元素之前 if (isUpdate) { const originChild = childNodes[i]; if (originChild !== dom) { parent.insertBefore(dom, originChild); } } } // 清理剩下的未使用的dom元素 if (nodesWithKeyCount) { for (key in nodesWithKey) { const node = nodesWithKey[key]; if (node !== undefined) { node.parentNode.removeChild(node); } } } // 清理剩下的未使用的dom元素 while (min <= nodesWithoutKeyCount) { const node = nodesWithoutKey[nodesWithoutKeyCount--]; if ( node !== undefined) { node.parentNode.removeChild(node); } } }
代碼比較長,主要是以下幾個步驟:
將所有dom子元素分為有key和沒key兩組
遍歷VD子元素,如果VD子元素有key,則去查找有key的分組;如果沒key,則去沒key的分組找一個類型相同的元素出來
diff一下,得出是否更新元素的類型
如果是更新元素且子元素不是原來的,則移動元素
最后清理刪除沒用上的dom子元素
diff也要改造一下,如果是新建、刪除或者替換元素,返回false。更新元素則返回true:
function diff(dom, newVDom, parent) { // 新建node if (dom == undefined) { parent.appendChild(createElement(newVDom)); return false; } // 刪除node if (newVDom == undefined) { parent.removeChild(dom); return false; } // 替換node if (!isSameType(dom, newVDom)) { parent.replaceChild(createElement(newVDom), dom); return false; } // 更新node if (dom.nodeType === Node.ELEMENT_NODE) { // 比較props的變化 diffProps(newVDom, dom); // 比較children的變化 diffChildren(newVDom, dom); } return true; }
為了看效果,view函數也要改造下:
const arr = [0, 1, 2, 3, 4]; function view() { const elm = arr.pop(); // 用于測試能不能正常刪除元素 if (state.num !== 9) arr.unshift(elm); // 用于測試能不能正常添加元素 if (state.num === 12) arr.push(9); return (Hello World); }{ arr.map( i => (
- 第{i}
)) }
通過變換數組元素的順序和適時的添加/刪除元素,驗證了代碼按照我們的設計思路正確運行。
四、總結本文基于上一個版本的代碼,加入了對唯一標識(key)的支持,很好的提高了更新數組元素的效率。基于當前這個版本的代碼還能做怎樣的優化呢,請看下一篇的內容:你不知道的Virtual DOM(五):自定義組件。
P.S.: 想看完整代碼見這里,如果有必要建一個倉庫的話請留言給我:代碼
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97155.html
摘要:經過這次優化,計算的時間快了那么幾毫秒。基于當前這個版本的代碼還能做怎樣的優化呢,請看下一篇的內容你不知道的四的作用。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的兩大前端框架,React和Vue,都不約而同的借助Virtual DOM技術提高頁面的渲染效率。那么,什...
摘要:變化的只有種更新和刪除。頁面的元素的數量隨著而變。四總結本文詳細介紹如何實現一個簡單的算法,再根據計算出的差異去更新真實的。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的兩大前端框架,React 和 Vue,都不約而同的借助 Virtual DOM 技術提高頁面的渲染...
摘要:不同的框架對這三個屬性的命名會有點差別,但表達的意思是一致的。它們分別是標簽名屬性和子元素對象。我們先來看下頁面的更新一般會經過幾個階段。元素有可能是數組的形式,需要將數組解構一層。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的兩大前端框架,React和Vue,都不約...
摘要:如果列表是空的,則存入組件后將異步刷新任務加入到事件循環當中。四總結本文基于上一個版本的代碼,加入了事件處理功能,同時通過異步刷新的方法提高了渲染效率。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的兩大前端框架,React和Vue,都不約而同的借助Virtual DO...
摘要:現在流行的前端框架都支持自定義組件,組件化開發已經成為提高前端開發效率的銀彈。二對自定義組件的支持要想正確的渲染組件,第一步就是要告訴某個標簽是自定義組件。下面的例子里,就是一個自定義組件。解決了識別自定義標簽的問題,下一步就是定義標簽了。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、...
閱讀 1572·2021-11-25 09:43
閱讀 2476·2019-08-30 15:54
閱讀 2938·2019-08-30 15:53
閱讀 1087·2019-08-30 15:53
閱讀 747·2019-08-30 15:52
閱讀 2538·2019-08-26 13:36
閱讀 806·2019-08-26 12:16
閱讀 1210·2019-08-26 12:13