专业企业建站公司,织梦和wordpress哪个文件,百度商桥怎样绑定网站,外国媒体网站什么是虚拟DOM
讲Diff算法前#xff0c;我先给大家讲一讲什么是虚拟DOM吧。这有利于后面大家对Diff算法的理解加深。
虚拟DOM是一个对象#xff0c;一个什么样的对象呢#xff1f;一个用来表示真实DOM的对象#xff0c;要记住这句话。我举个例子#xff0c;请看以下真实…什么是虚拟DOM
讲Diff算法前我先给大家讲一讲什么是虚拟DOM吧。这有利于后面大家对Diff算法的理解加深。
虚拟DOM是一个对象一个什么样的对象呢一个用来表示真实DOM的对象要记住这句话。我举个例子请看以下真实DOM
ul idlistli classitem哈哈/lili classitem呵呵/lili classitem嘿嘿/li
/ul
对应的虚拟DOM为
let oldVDOM { // 旧虚拟DOMtagName: ul, // 标签名props: { // 标签属性id: list},children: [ // 标签子节点{tagName: li, props: { class: item }, children: [哈哈]},{tagName: li, props: { class: item }, children: [呵呵]},{tagName: li, props: { class: item }, children: [嘿嘿]},]}
这时候我修改一个li标签的文本
ul idlistli classitem哈哈/lili classitem呵呵/lili classitem哈哈哈哈哈/li // 修改
/ul
这时候生成的新虚拟DOM为
let newVDOM { // 新虚拟DOMtagName: ul, // 标签名props: { // 标签属性id: list},children: [ // 标签子节点{tagName: li, props: { class: item }, children: [哈哈]},{tagName: li, props: { class: item }, children: [呵呵]},{tagName: li, props: { class: item }, children: [哈哈哈哈哈]},]} 这就是咱们平常说的新旧两个虚拟DOM这个时候的新虚拟DOM是数据的最新状态那么我们直接拿新虚拟DOM去渲染成真实DOM的话效率真的会比直接操作真实DOM高吗那肯定是不会的看下图 由上图一看便知肯定是第2种方式比较快因为第1种方式中间还夹着一个虚拟DOM的步骤所以虚拟DOM比真实DOM快这句话其实是错的或者说是不严谨的。
那正确的说法是什么呢虚拟DOM算法操作真实DOM性能高于直接操作真实DOM虚拟DOM和虚拟DOM算法是两种概念。虚拟DOM算法 虚拟DOM Diff算法
什么是Diff算法
上面咱们说了虚拟DOM也知道了只有虚拟DOM Diff算法才能真正的提高性能那讲完虚拟DOM我们再来讲讲Diff算法吧还是上面的例子(这张图被压缩的有点小大家可以打开看比较清晰) 上图中其实只有一个li标签修改了文本其他都是不变的所以没必要所有的节点都要更新只更新这个li标签就行Diff算法就是查出这个li标签的算法。
总结Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM对比出是哪个虚拟节点更改了找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点而不用更新其他数据没发生改变的节点实现精准地更新真实DOM进而提高效率。
使用虚拟DOM算法的损耗计算总损耗 虚拟DOM增删改与Diff算法效率有关真实DOM差异增删改较少的节点排版与重绘
直接操作真实DOM的损耗计算总损耗 真实DOM完全增删改可能较多的节点排版与重绘
Diff算法的原理
Diff同层对比
新旧虚拟DOM对比的时候Diff算法比较只会在同层级进行, 不会跨层级比较。所以Diff算法是:深度优先算法。时间复杂度:O(n) Diff对比流程
当数据改变时会触发setter并且通过Dep.notify去通知所有订阅者Watcher订阅者们就会调用patch方法给真实DOM打补丁更新相应的视图。
newVnode和oldVnode同层的新旧虚拟节点 patch方法
这个方法作用就是对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准下面会讲)
是继续执行patchVnode方法进行深层比对否没必要比对了直接整个节点替换成新虚拟节点
来看看patch的核心原理代码
function patch(oldVnode, newVnode) {// 比较是否为一个类型的节点if (sameVnode(oldVnode, newVnode)) {// 是继续进行深层比较patchVnode(oldVnode, newVnode)} else {// 否const oldEl oldVnode.el // 旧虚拟节点的真实DOM节点const parentEle api.parentNode(oldEl) // 获取父节点createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点if (parentEle ! null) {api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点// 设置null释放内存oldVnode null}}return newVnode
} sameVnode方法
patch关键的一步就是sameVnode方法判断是否为同一类型节点那问题来了怎么才算是同一类型节点呢这个类型的标准是什么呢
咱们来看看sameVnode方法的核心原理代码就一目了然了
function sameVnode(oldVnode, newVnode) {return (oldVnode.key newVnode.key // key值是否一样oldVnode.tagName newVnode.tagName // 标签名是否一样oldVnode.isComment newVnode.isComment // 是否都为注释节点isDef(oldVnode.data) isDef(newVnode.data) // 是否都定义了datasameInputType(oldVnode, newVnode) // 当标签为input时type必须是否相同)
} patchVnode方法
这个函数做了以下事情
找到对应的真实DOM称为el判断newVnode和oldVnode是否指向同一个对象如果是那么直接return如果他们都有文本节点并且不相等那么将el的文本节点设置为newVnode的文本节点。如果oldVnode有子节点而newVnode没有则删除el的子节点如果oldVnode没有子节点而newVnode有则将newVnode的子节点真实化之后添加到el如果两者都有子节点则执行updateChildren函数比较子节点这一步很重要
function patchVnode(oldVnode, newVnode) {const el newVnode.el oldVnode.el // 获取真实DOM对象// 获取新旧虚拟节点的子节点数组const oldCh oldVnode.children, newCh newVnode.children// 如果新旧虚拟节点是同一个对象则终止if (oldVnode newVnode) return// 如果新旧虚拟节点是文本节点且文本不一样if (oldVnode.text ! null newVnode.text ! null oldVnode.text ! newVnode.text) {// 则直接将真实DOM中文本更新为新虚拟节点的文本api.setTextContent(el, newVnode.text)} else {// 否则if (oldCh newCh oldCh ! newCh) {// 新旧虚拟节点都有子节点且子节点不一样// 对比子节点并更新updateChildren(el, oldCh, newCh)} else if (newCh) {// 新虚拟节点有子节点旧虚拟节点没有// 创建新虚拟节点的子节点并更新到真实DOM上去createEle(newVnode)} else if (oldCh) {// 旧虚拟节点有子节点新虚拟节点没有//直接删除真实DOM里对应的子节点api.removeChild(el)}}
} 其他几个点都很好理解我们详细来讲一下updateChildren
updateChildren方法
这是patchVnode里最重要的一个方法新旧虚拟节点的子节点对比就是发生在updateChildren方法中接下来就结合一些图来讲让大家更好理解吧
是怎么样一个对比方法呢就是首尾指针法新的子节点集合和旧的子节点集合各有首尾两个指针举个例子
ullia/lilib/lilic/li
/ul修改数据后ullib/lilic/lilie/lilia/li
/ul 那么新旧两个子节点集合以及其首尾指针为 然后会进行互相进行比较总共有五种比较情况
1、oldS 和 newS使用sameVnode方法进行比较sameVnode(oldS, newS)2、oldS 和 newE使用sameVnode方法进行比较sameVnode(oldS, newE)3、oldE 和 newS使用sameVnode方法进行比较sameVnode(oldE, newS)4、oldE 和 newE使用sameVnode方法进行比较sameVnode(oldE, newE)5、如果以上逻辑都匹配不到再把所有旧子节点的 key 做一个映射到旧节点下标的 key - index 表然后用新 vnode 的 key 去找出在旧节点中可以复用的位置。接下来就以上面代码为例分析一下比较的过程
分析之前请大家记住一点最终的渲染结果都要以newVDOM为准这也解释了为什么之后的节点移动需要移动到newVDOM所对应的位置 第一步
oldS a, oldE c newS b, newE a
比较结果oldS 和 newE 相等需要把节点a移动到newE所对应的位置也就是末尾同时oldSnewE-- 第二步
oldS b, oldE c newS b, newE e
比较结果oldS 和 newS相等需要把节点b移动到newS所对应的位置同时oldS,newS 第三步
oldS c, oldE c newS c, newE e
比较结果oldS、oldE 和 newS相等需要把节点c移动到newS所对应的位置同时oldS,newS 第四步
oldS oldE则oldCh先遍历完成了而newCh还没遍历完说明newCh比oldCh多所以需要将多出来的节点插入到真实DOM上对应的位置上 思考题
我在这里给大家留一个思考题哈。上面的例子是newCh比oldCh多假如相反是oldCh比newCh多的话那就是newCh先走完循环然后oldCh会有多出的节点结果会在真实DOM里进行删除这些旧节点。
大家可以自己思考一下模拟一下这个过程像我一样画图模拟才能巩固上面的知识。
附上updateChildren的核心原理代码
function updateChildren(parentElm, oldCh, newCh) { let oldStartIdx 0, newStartIdx 0 let oldEndIdx oldCh.length - 1 let oldStartVnode oldCh[0] let oldEndVnode oldCh[oldEndIdx] let newEndIdx newCh.length - 1 let newStartVnode newCh[0] let newEndVnode newCh[newEndIdx] let oldKeyToIdx let idxInOld let elmToMove let before while (oldStartIdx oldEndIdx newStartIdx newEndIdx) { if (oldStartVnode null) { oldStartVnode oldCh[oldStartIdx] } else if (oldEndVnode null) { oldEndVnode oldCh[--oldEndIdx] } else if (newStartVnode null) { newStartVnode newCh[newStartIdx] } else if (newEndVnode null) { newEndVnode newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) oldStartVnode oldCh[oldStartIdx] newStartVnode newCh[newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) oldEndVnode oldCh[--oldEndIdx] newEndVnode newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) oldStartVnode oldCh[oldStartIdx] newEndVnode newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) oldEndVnode oldCh[--oldEndIdx] newStartVnode newCh[newStartIdx] } else { // 使用key时的比较 if (oldKeyToIdx undefined) { oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表 } idxInOld oldKeyToIdx[newStartVnode.key] if (!idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode newCh[newStartIdx] } else { elmToMove oldCh[idxInOld] if (elmToMove.sel ! newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) } else { patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] null api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) } newStartVnode newCh[newStartIdx] } } } if (oldStartIdx oldEndIdx) { before newCh[newEndIdx 1] null ? null : newCh[newEndIdx 1].el addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) } else if (newStartIdx newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) }
}用index做key
平常v-for循环渲染的时候为什么不建议用index作为循环项的key呢
我们举个例子左边是初始数据然后我在数据前插入一个新数据变成右边的列表
ul ul li key0a/li li key0/li li key1b/li li key1a/li li key2c/li li key2b/li li key3c/li
/ul /ul 按理说最理想的结果是只插入一个li标签新节点其他都不动确保操作DOM效率最高。但是我们这里用了index来当key的话真的会实现我们的理想结果吗废话不多说实践一下
ul li v-for(item, index) in list :keyindex{{ item.title }}/li/ulbutton clickadd增加/buttonlist: [ { title: a, id: 100 }, { title: b, id: 101 }, { title: c, id: 102 }, ] add() { this.list.unshift({ title: , id: 99 }); } 点击按钮我们可以看到并不是我们预想的结果而是所有li标签都更新了 为什么会这样呢还是通过图来解释
按理说abc三个li标签都是复用之前的因为他们三个根本没改变改变的只是前面新增了一个林三心 但是我们前面说了在进行子节点的 diff算法 过程中会进行 旧首节点和新首节点的sameNode对比这一步命中了逻辑因为现在新旧两次首部节点 的 key 都是 0了同理key为1和2的也是命中了逻辑导致相同key的节点会去进行patchVnode更新文本
而原本就有的c节点却因为之前没有key为4的节点而被当做了新节点所以很搞笑使用index做key最后新增的居然是本来就已有的c节点。所以前三个都进行patchVnode更新文本最后一个进行了新增那就解释了为什么所有li标签都更新了。 那我们可以怎么解决呢其实我们只要使用一个独一无二的值来当做key就行了
ul li v-foritem in list :keyitem.id{{ item.title }}/li /ul
现在再来看看效果 为什么用了id来当做key就实现了我们的理想效果呢因为这么做的话abc节点的key就会是永远不变的更新前后key都是一样的并且又由于abc节点的内容本来就没变
所以就算是进行了patchVnode也不会执行里面复杂的更新操作节省了性能而林三心节点由于更新前没有他的key所对应的节点所以他被当做新的节点增加到真实DOM上去了。