Diff 算法核心原理

什么是虚拟DOM

讲Diff算法前,我先给大家讲一讲什么是虚拟DOM吧。这有利于后面大家对Diff算法的理解加深。

虚拟DOM是一个对象,一个什么样的对象呢?一个用来表示真实DOM的对象,要记住这句话。我举个例子,请看以下真实DOM:

<ul id="list"><li class="item">哈哈</li><li class="item">呵呵</li><li class="item">嘿嘿</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 id="list"><li class="item">哈哈</li><li class="item">呵呵</li><li class="item">哈哈哈哈哈</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方法中,接下来就结合一些图来讲,让大家更好理解吧

是怎么样一个对比方法呢?就是首尾指针法,新的子节点集合和旧的子节点集合,各有首尾两个指针,举个例子:

<ul><li>a</li><li>b</li><li>c</li>
</ul>修改数据后<ul><li>b</li><li>c</li><li>e</li><li>a</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所对应的位置,也就是末尾,同时oldS++,newE--

  • 第二步

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 key="0">a</li>        <li key="0"></li>    <li key="1">b</li>        <li key="1">a</li>   <li key="2">c</li>        <li key="2">b</li>       <li key="3">c</li>
</ul>                     </ul>

按理说,最理想的结果是:只插入一个li标签新节点,其他都不动,确保操作DOM效率最高。但是我们这里用了index来当key的话,真的会实现我们的理想结果吗?废话不多说,实践一下:

<ul>  <li v-for="(item, index) in list" :key="index">{{ item.title }}</li></ul><button @click="add">增加</button>list: [      { title: "a", id: "100" },      { title: "b", id: "101" },      { title: "c", id: "102" },    ]    add() {    this.list.unshift({ title: "", id: "99" });  }

点击按钮我们可以看到,并不是我们预想的结果,而是所有li标签都更新了

为什么会这样呢?还是通过图来解释

按理说,a,b,c三个li标签都是复用之前的,因为他们三个根本没改变,改变的只是前面新增了一个林三心

但是我们前面说了,在进行子节点的 diff算法 过程中,会进行 旧首节点和新首节点的sameNode对比,这一步命中了逻辑,因为现在新旧两次首部节点 的 key 都是 0了,同理,key为1和2的也是命中了逻辑,导致相同key的节点会去进行patchVnode更新文本,

而原本就有的c节点,却因为之前没有key为4的节点,而被当做了新节点,所以很搞笑,使用index做key,最后新增的居然是本来就已有的c节点。所以前三个都进行patchVnode更新文本,最后一个进行了新增,那就解释了为什么所有li标签都更新了。

那我们可以怎么解决呢?其实我们只要使用一个独一无二的值来当做key就行了

<ul>  
   <li v-for="item in list" :key="item.id">{{ item.title }}</li>
</ul>

现在再来看看效果

为什么用了id来当做key就实现了我们的理想效果呢,因为这么做的话,a,b,c节点的key就会是永远不变的,更新前后key都是一样的,并且又由于a,b,c节点的内容本来就没变,

所以就算是进行了patchVnode,也不会执行里面复杂的更新操作,节省了性能,而林三心节点,由于更新前没有他的key所对应的节点,所以他被当做新的节点,增加到真实DOM上去了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/357334.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 列表 字典 读写文件:pickle模块的基本使用

建议大家使用cPickle&#xff0c;速度更快&#xff01;&#xff01;&#xff01; python数据持久存储&#xff1a;pickle模块的基本使用&#xff08;转载&#xff09; 作者: pzxbc出处: http://pzxbc.cnblogs.com/本文版权归作者和博客园共有&#xff0c;欢迎转载&#xff0c;但…

first-child、first-of-type等属性的差别

1、xxx:first-child 伪类 xxx作为第一个子元素 //p元素作为第一个子元素的都会变色 p:first-child { background:yellow; } //p作为父元素的 第一个子元素。不要求第一个元素的类型 p>:first-child {background:yellow; }//p作为父元素的 第一个子元素。要求第一个元素的类…

我的Serverless实战——引领云计算的下一个十年

前言&#xff1a;如今&#xff0c;越来越多的大厂企业开始大规模使用Serverless&#xff0c;处于变革中的开发者&#xff0c;大多已从观望状态转向尝试阶段&#xff0c;越来越多Serverless落地场景被解锁。作为基础研发底座&#xff0c;越来越多企业开始接受Serverless&#xf…

各种边距clientWidth、offsetWidth、scrollWidth、clientLeft、getBoundingClientRect详解

1、clientWidth、offsetWidth、scrollWidth <!DOCTYPE html> <html><head><meta charset"utf-8" /><style>#box1 {padding: 50px;position: static;}#box {border: 1px solid red;overflow: scroll;height: 200px;width: 500px;}#con…

花了两天时间用html+css+js做了一个网页版坦克大战游戏

大家好&#xff0c;我是孙叫兽&#xff0c;本期内容给大家分享如何用htmlcssjavaScript去做一个简易网页版坦克游戏。 目录 坦克游戏玩法及介绍 项目结构 源码地址&#xff1a; 坦克游戏玩法及介绍 我们先来看一下首页。 打开这个首页很简单&#xff0c;基本是上面这个样子&…

设置文字多行显示溢出显示省略号

#news_text { border: 1px solid red; width: 200px; word-break: break-all; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; /*这里可以设置文本显示的行数*/ overflow: hidden; }<div id"news_text"&g…

vue项目有几个接口content Download时间特别长的解决办法

做的可视化地图项目&#xff0c;有几个接口加载比较慢&#xff0c;大概16S左右&#xff0c;第一次加载接近一分钟&#xff0c;这谁能受得了。 对比了正式环境和测试环境&#xff0c;这个接口数据量一样&#xff0c;就是在测试服务器比较慢&#xff0c;排除滚动插件及其它的影响…

Jmeter调试工具---HTTP Mirror Server

转自&#xff1a;http://www.cnblogs.com/puresoul/p/4907655.html 一、HTTP Mirror Server的作用&#xff1a; 它可以在本地临时搭建一个HTTP服务器&#xff0c;该服务器把接收到的请求原样返回&#xff0c;这样就可以看到发送出的请求的具体内容&#xff0c;以供调试。 二、…

520 单身福利|女朋友又找我要礼物

大家好&#xff0c;我是孙叫兽&#xff0c;520快到了&#xff0c;给大家分享一下单身福利&#xff01;时间过得真快&#xff0c;一转眼又到了 520&#xff0c;有对象的程序员又激动又扎心的节日&#xff0c;其实吧&#xff0c;程序员对这种节日又期待又焦灼&#xff0c;为什么呢…

pit和systick_PIT和TestNG突变测试简介

pit和systick变异测试是一种技术&#xff0c;它可以发现测试未涵盖代码的哪些部分。 它类似于代码覆盖范围 &#xff0c;但变异测试不限于在测试期间执行给定行的事实。 这个想法是修改生产代码&#xff08;引入突变&#xff09;&#xff0c;这应该改变其行为&#xff08;产生不…

使用vue-seamless-scroll自动滚动插件复制出来的数据点击事件无效的解决办法

引言&#xff1a;做可视化地图项目&#xff0c;使用vue-seamless-scroll实现表格数据自动滚动&#xff0c;有个bug就是复制出来的数据点击事件失效。这个插件底层的实现是cope的形式&#xff0c; 无限滚动原理&#xff1a;无限滚动的原理就是把之前的遍历的内容复制一份&#x…

最短路的几种算法及其优化(模板)

一.Dijkstra 算法 dijkstra算法适用于边权为正的情况&#xff0c;求单源最短路&#xff0c;适用于有向图和无向图 模板伪代码&#xff1a; 清除所有点的标号 设d[0]0&#xff0c;其余d[i]INF&#xff1b; 循环n次{ 在所有未标记的节点中&#xff0c;寻找d[i]最小的点x 给x做标记…

极客青年说,北京沙龙

大家好&#xff0c;我是孙叫兽&#xff0c;本期内容给大家分享infoq写作平台在北京站的沙龙主题。主要流程如下&#xff1a;早上起来&#xff0c;吃了点饭&#xff0c;然后就去北京望京悠乐汇的A1202,这次是个轰趴馆&#xff0c;比较适合年轻的程序员放松。去的稍微早了点&…

vue点击弹窗自动触发点击事件的解决办法

业务场景&#xff1a;使用vue element ui 的el-dialog&#xff0c;点击弹窗之后&#xff0c;默认加载第一个按钮的数据进行初始化。 div 指令&#xff1a; // 自动触发点击事件directives:{trigger:{inserted(el,binging){// console.log("自动触发事件")el.click()}…

vue根据表格字段不同的状态显示不同的颜色。

业务需求:根据后台返回的数据,对表格中的严重等级和问题状态做一下颜色区分。数据很大,大概一年左右的数据,在二级弹窗中,数据滚动的形式。 大家好,我是孙叫兽 不加状态前: 我这个使用vue+div循环的实现,很便捷,使用element ui也是可以的。 然后再computed:{}中添加…

Apache JMeter教程

要负载测试您的Web应用程序吗&#xff1f; 然后&#xff0c;您应该了解Apache JMeter &#xff01; 该JMeter教程介绍了基本概念以及如何创建测试计划以及如何执行它以对应用程序进行基准测试。 这是我在Disy的Tech-Blog上的文章的交叉张贴 -谢谢您让我在公司时间写文章&…

Echats给柱状图及提示文字添加百分号(%)的解决办法

业务需求&#xff1a;给柱状图添加百分号。 目录 柱状图上添加%效果图 悬浮标签添加%效果图&#xff1a; 刚开始从后台取的数据带%&#xff0c;我这边取这个值的时候显示undifined&#xff0c; 后来就让这哥们把这类的数据从数据库把%去掉。这样我这边就取到了数据&#xff0…

前端使用linux命令更新项目生产包与测试包命令

业务需求&#xff1a;把vue开发的项目打成dist.zip文件&#xff0c;丢到服务器去&#xff0c;通过域名进行访问。 首先登录云管平台的账号和密码&#xff0c;找到对应的服务器&#xff0c;然后连接到堡垒机。 在命令行进入到域名下的地址及目录。 切换到测试文件夹 点击左上角…

CSDN学习神器——CSDN浏览器助手测评体验

导读&#xff1a;大家好&#xff0c;我叫孙叫兽&#xff0c;本期内容给大家分享一下CSDN浏览器助手这款插件&#xff0c;看看最新版的插件已经比较完善了&#xff0c;还有一些可以优化的地方。下面简单进行体验一把最新版&#xff01; 体验时间&#xff1a;2021年5月31日。 目录…

CVE-2016-0143 漏洞分析(2016.4)

CVE-2016-0143漏洞分析 0x00 背景 4月20日&#xff0c;Nils Sommer在exploitdb上爆出了一枚新的Windows内核漏洞PoC。该漏洞影响所有版本的Windows操作系统&#xff0c;攻击者利用成功后可获得权限提升&#xff0c;微软在4月补丁日修复了该漏洞。 0x01 漏洞分析 Nils Sommer并没…