vue框架里面的两大核心,虚拟dom和数据双向绑定原理,数据双向绑定原理已经在我的另外一篇博客中详细介绍了,本文来了解一下虚拟dom以及与虚拟dom难离难舍的diff算法。
一、Vue的虚拟dom:
Virtual dom,也就是我们常说的虚拟节点,它是通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM的节点。
真实DOM的代码:
<div>
<p>test</p>
</div>
虚拟DOM的伪代码:
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: 'test' }
]
为什么要引入虚拟dom?
概括:解决浏览器性能问题,减少重排,提高性能。
再回顾一下浏览器渲染页面的流程:
(1)HTML被HTML解析器解析成DOM Tree, css则被css解析器解析成CSSOM Tree(并行解析)。
(2)DOM Tree和CSSOM Tree解析完成后,被合并到一起,形成渲染树(render Tree)。
(3)重排:节点信息计算,即根据渲染树计算每个节点的几何信息(大小及位置)。
(4)重绘:渲染绘制,即根据计算好的信息绘制整个页面,渲染出最终的页面。
总结:每次真实 dom 发生改变引起重排都会将上面的流程跑一遍,而重排过程特别是计算节点信息是非常消耗性能的,于是我们引入vdom,在vdom上进行的操作不会引起重排,然后再通过diff算法比较新vdom(修改之后的)和旧 vdom(修改前的)的不同从而去更新真实dom(patch方法)。
注意:特别要提一下 Vue 的 patch 是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理),这样岂不是相当于没有优化?实际上现代浏览器对这样的DOM操作做了优化,所以表现出来的结果是一样的,即减少的操作真实dom的次数,达到减少重排,提高性能的目的。
二、Vue的diff算法
用于高效更新 dom 的一套算法
原理解析:
经典核心原理图:递归地使用diff算法来比较同一层级的 node
(图片转自第一篇参考文章)
diff 算法在执行时有三个维度,分别是Tree diff、Component diff 和Element diff,执行时按顺序依次执行,它们的差异仅仅因为 diff 粒度不同、执行先后顺序不同
vue中 diff 算法的大致思路:
(为了显得不那么抽象,贴一张移动了一次首指针之后的图,来自参考文章)
核心就是循环进行头尾节点比较,终止条件就是新旧vdom有一方首尾指针相遇。
新旧vdom分别有两个首尾指针一开始时指向首尾节点,这里我用:新头、新尾、旧头、旧尾表示。然后对其指向的节点进行四次比较:新头—旧头、新尾—旧尾、新尾—旧头、新头—旧尾。
循环过程:
-
第一步 头头比较。若相似(指tag一致,比如都是div),旧头新头指针后移,真实dom不变,进入下一次循环;不相似,进入第二步。
-
第二步 尾尾比较。若相似,旧尾新尾指针前移,真实dom不变,进入下一次循环;不相似,进入第三步。
-
第三步 新尾旧头比较。若相似,旧头指针后移,新尾指针前移,真实dom序列中的头移到尾(此时的头尾指向节点),进入下一次循环;不相似,进入第四步。
-
第四步 新头旧尾比较。若相似,旧尾指针前移,新头指针后移,真实dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。
-
第五步 若新头节点有 key 且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移;否则,新头对应的节点插入当前真实dom序列的头部,新头指针后移。
注:终止循环后如果只有一方的首尾指针相遇,就要进行增删操作,例如是新 vdom 首尾指针未相遇,就要在真实dom上增加新节点,反之为删。
经常与v-for搭配使用的 key 在这里充当什么角色呢?
key的作用主要是为了更加高效地更新虚拟dom:
由于v-for大部分情况下生成的都是相同tag的标签,如果没有key标识,那么相当于每次头头比较都能成功。也就不会有第五步里有key时的插入操作。
根据上图的例子理解:
原序列是:a,b,c,d,e
新序列:a,b,c,z,d,e
我们当然知道直接在c和d之间直接插入z就可以了但是没有设置key时,diff算法它是不知道的,于是执行的过程就会是将d换成z,将e换成d,再添加一个e;当数据量较大时,明显多了很多不必要的操作,效率较低。
总结:有key的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!
用index作为key会发生什么?(划重点,字节面试题)
用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作(类名、样式、指令,那么都会被全量的更新)。
注:用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个命周期中都保持稳定。
没有设置key时就地复用问题
“就地复用”官方的解释是:
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出
实在是太抽象了,这里我就用表单输入值的例子简单说明一下,例如有一个可以切换输入语言格式的输入框,即设置一个按钮可以切换输入中文或英文,当我们在中文状态下在输入框输入了一定的中文字体时,此时点击切换到英文输入状态,之前输入框中的中文字体会被带过去并展示出来,这就是就地复用原则导致的。
总之,要灵活使用!
关于diff算法这里我只总结一下大致的思路和需要注意的地方,具体的图示和细节推荐大家看 深入Vue2.x的虚拟DOM diff原理 这篇博客是我目前看到的关于diff算法讲得最简单易懂的一篇,自觉无法超越,极力推荐!
参考文章:
Vue 虚拟DOM和Diff算法
vue虚拟dom 以及diff 算法
深入Vue2.x的虚拟DOM diff原理