Fram/Lib

VUE中的虚拟DOM和Diff算法以及key值作用

字数:5966    阅读时间:30min
阅读量:2491

虚拟DOM

什么是虚拟DOM

对于真实dom和虚拟dom的个人理解:可以被浏览器直接渲染出来的节点称作是真实dom,不能被浏览器直接渲染,存在于内存中,具有描述节点属性的节点称为虚拟dom;在js中可以通过createElemen接口创建一个标签对象,然后通过为这个对象添加属性的方式来描述这个节点,这个节点就可以说是一个虚拟的dom。说白了虚拟dom就是一个存在于内存中的js对象

为什么需要虚拟dom

我们在修改页面dom数据的时候,改变了dom的样式或位置,就会引起浏览器的重绘乃至回流(重排),不管是重绘还是重排浏览器都需要重新渲染整个dom树,频繁的修改dom会大大消耗浏览器的性能。那么为了减轻浏览器的压力,提升用户体验,我们就要尽量少对页面真实dom的修改,虚拟dom也就应运而生。试想一下,如果有一个存在于内存中的虚拟dom,在我们需要修改dom数据的时候,只修改这个虚拟dom的属性,是不是就不会造成浏览器的回流和重绘,这就是减少对真实dom操作的一个突破口

虚拟DOM的优缺点

使用虚拟dom的优点很明显:主要是为了减少页面的回流和重绘,提升浏览器渲染性能和用户体验;另外因为脱离了真实dom,拥有跨平台能力;如果非要说缺点:那就是在内存中多了一份dom树,在没有大量、频繁修改dom元素数据的页面下,使用虚拟dom性能反而会下降

虚拟DOM的实现

我们可以试着自己写一个简单的虚拟dom,当然vue中有专门的createElm函数,如果感兴趣可以去了解一下

上述代码在控制台打印结果

diff算法

有了虚拟dom之后,我们就可以将对真实dom的操作,转化为对虚拟dom进行操作,然后再映射给真实dom。这样就相应的较少了浏览器重绘的次数。那么我们又如何知道需要修改的地方呢?在页面初始化时候,VUE就会将真实dom转换成一份虚拟dom,存在js内存中;当我们改变了某个dom的数据时,就会生成一个新的newVnode;VUE将会对比这个dom对应的Vnode和新生成的newVnode,来决定那些节点复用哪些节点更新;这种对比新旧节点寻找差异的方法就是今天要学习的diff算法(这里说的diff算法是VUE中优化后的算法)

diff算法的两个特点

diff算法有两个特点:1.对比对象遵循同级比较,只会对新旧节点的同级节点进行比较,不会跨级比较;2.对比的方式遵循两端开始向中间聚拢

diff算法流程

  • vue中diff的大概流程
    1. 改变dom数据 --- 触发observer观测函数里的setter方法 --- setter方法执行dep.notify(),通知所有watcher数据发生改变 --- watcher在update函数中调用patch方法给真实dom打补丁
    2. patch方法主要判断新旧节点是否为同一个节点
      • 如果是同一个节点:通过patchVnode方法,判断新旧节点是否都存在子节点
        • 如果都存在子节点,通过updateChildren方法更新子节点

VUE的diff算法是通过比较Vnode和个newVnode两个虚拟节点找出存在差异,然后根据具体情况复用或更新原真实dom;其实vue的diff最核心的就是patchpatchVnodeupdateChildren这三个函数,搞清楚了这三个函数都做了什么也就算是了解了VUE的diff算法核心

需要注意的是:VUE的diff算法是边比较,边更新;而不是比较完成之后,一次性反映给真实dom进行修改;至于为什么,个人理解:1.现代浏览器已经对操作dom做了优化;会把需要处理的dom放进一个队列,然后定时集中处理。VUE再进行归纳后集中处理似乎有点重复;2.边比较边更新有利于提升VUE的性能;如果是比较完成之后再一次性修改,那VUE不仅需要记录修改的内容,还要记录修改位置的父子关系,这样一来算法的复杂度就上来了,也更消耗VUE的性能

patch

在比较两个节点存在的差异的时候,首先会通过patch函数先比较这两个节点(主要是为了比较新旧节点是不是相同节点),然后根据不同情况做不同处理:

  1. 当oldVnode不存在时(比如在页面初始化的时候):根据Vnode数据直接创建一个新节点
  2. 当Vnode和oldVnode相同的时候:通过patchVnode函数比较子节点情况
  3. 当Vnode和oldVnode不同的时候:直接根据Vnode创建新节点,并销毁oldVnode

以下是patch的部分源码

patchVnode

通过patch比较后,当Vnode和oldVnode是同一个节点的时候,就需要通过patchVnode函数(主要是判断子节点情况),根据子节点的不同情况做不同处理

  1. Vnode和oldVnode都存在子节点:需要继续通过updateChildren函数来对比这些子节点
  2. 只有Vnode存在子节点:将Vnode子节点添加给原真实dom,并将原文本节点置为空
  3. 只有oldVnode存在子节点:在原真实dom上删除旧子节点
  4. 当有文本节点:Vnode没有就删除原真实dom的文本节点,Vnode有就把原真实节点的文本节点替换为Vnode的文本节点
updateChildren

判断了节点的子节点情况后,如果Vnode和oldVnode都存在子节点,那就使用updateChildren来比较新旧节点的子节点,这也是diff算法的核心,那么diff算法是如何智能复用相同节点,替换不同节点呢:

vue的diff是通过循环比较新旧节点的首尾节点是否为同一节点的来判断节点是否复用的。vue会把旧节点(Vnode)开始比较的节点叫做oldStartVnode,最后一个比较的节点叫做oldEndVnode,新节点(newVnode)也一样,开始比较的节点叫newStartVnode,最后比较的节点叫做newEndVnode;通过标记新旧节点的头尾节点的比较有四种比较方式:旧头 == 新头,旧尾 == 新尾,旧头 == 新尾,旧尾 == 新头; 通过四种比较得出差异,根据差异修改原dom

  1. oldStar == newStar:旧头与新头是相同节点,原dom不做任何改变
  2. oldEnd == newEnd:旧尾与新尾是相同节点,原dom不做任何改变
  3. oldStar == newEnd:旧头与新尾是相同节点,将旧头对应的节点移动到旧尾对应的节点后面
  4. oldEnd == newStar:旧尾与新头是相同节点,将旧尾对应的节点移动到旧头对应的节点前面

为了更清楚的了解节点的移动情况,分别对这四种比较方式,通过画图的方式来帮助理解一下

1. oldStar == newStar 。旧头和新头相同,不做原任何改变
2. oldEnd == newEnd。旧尾与新尾是相同节点,原dom不做任何改变
3.oldStar == newEnd。旧头与新尾相同,将旧头对应的节点移动到旧尾对应的节点后面
4. oldEnd == newStar。旧尾与新头相同,将旧尾对应的节点移动到旧头对应的节点前面

如果通过以上四种方式都没匹配到相同的节点,我们就需要判断整个旧节点的子节点有没有该节点了。如果节点有key值,那就通过对比Vnode子节点生成的key值对照表,来找到这个相同的节点。如果没有key值旧只能遍历旧节点的所有子节点然后比较有没有相同节点了,这样做是比较消耗性能的。然后vue会根据有没有找到相同节点做不同处理:

  • 通过比较key值或者遍历所有子节点找到了newStar
    • 找到的节点和newStar是同一个节点:将该节点插入到oldStar前面
    • 不是同一节点: 根据newStar直接创建新节点
  • 没有找到newStar:根据newStar直接创建新节点
四种匹配方式无法找到匹配项,通过对比剩余key值对照表或循环剩余Vnode,在剩余Vnode中找到了该节点,移动该对应节点,插入到oldStar对应节点的前面
四种匹配方式无法找到匹配项,且剩余Vnode中没有该节点,根据该节点创建新的节点,并插入到oldStar对应节点的前面

以上步骤四种比较和判断所有剩余节点是否有该节点是一个循环,根据具体情况,移动真实dom对应的节点,并通过oldStartIdx...自增或自减,移动比较的节点位置(oldStart...)。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时就结束循环,并判断如果旧节点节点多余就删除多余节点,如果是新节点多出就在原dom上添加这些节点。以下是部分源码可做参考:

到这里VUE.2X的diff原理基本算是梳理完了;使用Xmind重新梳理了一个总体流程,用来帮助自己理解、复习diff的整个过程

key的作用

VUE中key值的作用可以从三个方面来说:节省性能方面、防止出错方面还有强制更新方面

节省性能方面

由上述diff的过程可以看出,如果节点设置有key值,那么我们在匹配相同节点进行复用的时候,就可以通过对比生成的key值哈希对照表,来找到对应的节点进行复用。如果没有key值,VUE就会拿当前节点和旧节点的子节点一一对比,来找有没有可以复用的相同节点,这会大大的消耗性能的。所以有key值的时候,在比较节点是否复用的时候,可以节省性能

但是官方文档又说:"不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素";也就是说不使用key的时候VUE会尽最大可能复用已有的元素,而当有key值的时候,因为多了一个key值是否相同的条件,就不能最大限度的复用已有元素。节点复用的性能当然比重建节点性能要高,如此看来有key值得时候反而降低了性能,但是仅凭一个tag就决定复用标签的话,很容易就会出错;

总体来说就是不用key会最大限度复用节点,性能最好,但是一不小心就会出错;用了key可以节约在比较是否复用上浪费的性能,而且也能更加准确的判断节点是否复用

防止出错方面

VUE中因为没有设置key值,而出的错,基本上都是因为VUE的'就地复用原则';那么到底什么是'就地复用原则'呢,在diff代码中看就是通过sameVnode函数决定节点复用的

'就地复用'字面意思就是,现场重复利用不再进行更新;官方给出的解释是:Vue在使用v-for渲染的列表时。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。 这种方式只适用于列表渲染不依赖子组件状态,或临时DOM状态变化。单看官方给出的解释,还是很难搞清楚什么是'就地复用策略';下面根据一个例子好好理解一下

上图可以看出,在采用就地复用的时候,虽然我们是将第一项移动到第二项,但是input里的值,似乎并没有跟着移动,而是留在了原处,成了原第二项的input的值;之所以会出现这种"错误",就是因为VUE算法中的'就地复用策略';我们可以通过结合这个例子,还原一下VUE的diff过程(以例子中的第一项为例):

当我们改变了列表的数据的时候,VUE生成新的节点和旧节点进行path:发现是新旧节点是相同节点(问题就出在这里);转而通过patchVnode比较该节点的子节点情况:发现新旧节点都存在子节点;然后又通过updateChildren来比较每个子节点:通过比较新旧节点的子节点发现input和button都是相同节点,不做任何处理保留下来,name节点发生改变,先将新name插入到对应位置,对比循环结束删除多余的旧name,此时'乐乐'就变成了'利利';所以这个节点的子节点就是name由'乐乐'变成'利利',但是input和button并没有改变还是原来的节点。

从以上的过程来看没有什么问题,但是执行下来就出错了,就是因为在比较新旧节点的时候sameVnode只比较了tag(标签名)和其他一些不太要紧的项,导致VUE认为新旧节点是相同节点,继续向下比较,以至于input等原节点被重复使用没有更新,最终出错;相反,在不采用就地复用的时候(其实就是为列表项添加了key值),新旧节点比较时,发现新节点的key值和旧节点key值不同,所以就认定新旧节点不是同一节点,然后就生成新节点替代旧节点,旧节点被删除,最终整个节点被更新;以下是上例的代码可做参考:

所以使用v-for渲染列表,列表项发生改变时就容易出错,所以官方建议使用v-for渲染列表时,必须写上key值;key值也不是随便设置的,key值的设置需具备唯一标识性,并不建议使用列表项的index作为key值;在使用index作为key值时,改变数据生成新节点时,新节点的key值仍是列表项的第几项,和旧节点的key值是一样的,这样一来新旧节点又是相同节点,VUE又开始复用可以复用的节点了,就跟没有设置key值是一样的;当然使用index作为key值,不排除在某些场景下不会出错,而且性能好也方便,但是为了程序的持久性和安全性考虑,还是不建议使用

强制更新方面

当节点设置key值,改变节点数据的时候,VUE不再向下进行patch比较,而是直接更新节点;利用这么一个特新,我们可以做到让节点或者组件强制更新;例如官方给出的一个例子:

上例中我们将text绑定为key值,当改变text值时,就会触发span标签强制更新,进而触发transition动画,动画就动起来了;

在发生需要我们手动更新的时候,一般是数据并不在响应系统中,或者数据改变之后节点(组件)没有更新;对于第一种情况:我们可以事先把数据名写在data中,以便VUE将数据添加到响应系统中,或者使用vm.$set(obj,key,value)方法将数据添加到响应系统;对于第二种情况我们可以:1、使用v-if渲染满足条件,执行渲染(并不推荐,浪费性能);2、使用vue提供的forceUpdate(较为推荐);3、组件设置key值,当改变key值得时候就会自动触发更新,key值不变则不更新(最好的办法)

到这里VUE的diff算法和key值的作用算是搞清楚了,但是VUE3.0已经发布了,相对于VUE2.X算法上也做了很大的优化和更新,那么成热打铁来了解一下VUE3.0更新后的diff算法吧

野生小园猿
励志做一只遨游在知识海洋里的小白鲨
查看“野生小园猿”的所有文章 →

发表评论

邮箱地址不会被公开。

相关推荐