歡迎關注個人公衆號睿Talk
,獲取我最新的文章:
javascript
目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提升頁面的渲染效率。那麼,什麼是Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一概用VD表示。html
這是VD系列文章的第四篇,如下是本系列其它文章的傳送門:
你不知道的Virtual DOM(一):Virtual Dom介紹
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新優化
你不知道的Virtual DOM(四):key的做用
你不知道的Virtual DOM(五):自定義組件
你不知道的Virtual DOM(六):事件處理&異步更新前端
今天,咱們繼續在以前項目的基礎上進行優化。用過React或者Vue的朋友都知道在渲染數組元素的時候,編譯器會提醒加上key這個屬性,那麼key是用來作什麼的呢?java
在渲染數組元素時,它們通常都有相同的結構,只是內容有些不一樣而已,好比:node
<ul> <li> <span>商品:蘋果</span> <span>數量:1</span> </li> <li> <span>商品:香蕉</span> <span>數量:2</span> </li> <li> <span>商品:雪梨</span> <span>數量:3</span> </li> </ul>
能夠把這個例子想象成一個購物車。此時若是想往購物車裏面添加一件商品,性能不會有任何問題,由於只是簡單的在ul的末尾追加元素,前面的元素都不須要更新:git
<ul> <li> <span>商品:蘋果</span> <span>數量:1</span> </li> <li> <span>商品:香蕉</span> <span>數量:2</span> </li> <li> <span>商品:雪梨</span> <span>數量:3</span> </li> <li> <span>商品:橙子</span> <span>數量:2</span> </li> </ul>
可是,若是我要刪除第一個元素,根據VD的比較邏輯,後面的元素所有都要進行更新的操做。dom結構簡單還好說,若是是一個複雜的結構,那頁面渲染的性能將會受到很大的影響。github
<ul> <li> <span>商品:香蕉</span> <span>數量:2</span> </li> <li> <span>商品:雪梨</span> <span>數量:3</span> </li> <li> <span>商品:橙子</span> <span>數量:2</span> </li> </ul>
有什麼方式能夠下降這種性能的損耗呢?算法
最直觀的方法確定是直接刪除第一個元素而後其它元素保持不變了。但程序沒有這麼智能,能夠像咱們同樣一眼就看出變化。程序能作到的是儘可能少的修改元素,經過移動元素而不是修改元素來達到更新的目的。爲了告訴程序要怎麼移動元素,咱們必須給每一個元素加上一個惟一標識,也就是key。segmentfault
<ul> <li key="apple"> <span>商品:蘋果</span> <span>數量:1</span> </li> <li key="banana"> <span>商品:香蕉</span> <span>數量:2</span> </li> <li key="pear"> <span>商品:雪梨</span> <span>數量:3</span> </li> <li key="orange"> <span>商品:橙子</span> <span>數量:2</span> </li> </ul>
當把蘋果刪掉的時候,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); } } }
代碼比較長,主要是如下幾個步驟:
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 ( <div> Hello World <ul myText="dickens"> { arr.map( i => ( <li id={i} class={`li-${i}`} key={i}> 第{i} </li> )) } </ul> </div> ); }
經過變換數組元素的順序和適時的添加/刪除元素,驗證了代碼按照咱們的設計思路正確運行。
本文基於上一個版本的代碼,加入了對惟一標識(key
)的支持,很好的提升了更新數組元素的效率。基於當前這個版本的代碼還能作怎樣的優化呢,請看下一篇的內容:你不知道的Virtual DOM(五):自定義組件。
P.S.: 想看完整代碼見這裏,若是有必要建一個倉庫的話請留言給我:代碼