欢迎关注个人公众号睿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.: 想看完整代码见这里,若是有必要建一个仓库的话请留言给我:代码