写在后面
在前端中,次要波及的基本上就是 DOM 的相干操作 和 JS,咱们都晓得 DOM 操作是比拟耗时的,那么在咱们写前端相干代码的时候,如何缩小不必要的 DOM 操作便成了前端优化的重要内容。
虚构 DOM(virtual DOM)
在 jQuery 时代,基本上所有的 DOM 相干的操作都是由咱们本人编写(当然博主是没有写过 jQuery 滴,可能因为博主太年老了吧,错过了 jQuery 大法的时代),如何操作 DOM, 操作 DOM 的机会应该如何安顿成了决定性能的要害,而到了 Vue、React 这些框架流行的时代,框架采纳数据驱动视图,封装了大量的 DOM 操作细节,使得更多的 DOM 操作细节的优化从开发者本人抉择、管制转移到了框架外部,那么在学会应用框架后,如果想要更加深刻学习框架,那就须要搞懂框架封装的底层原理,其中十分外围的一部分就是虚构 DOM(virtual DOM)
什么是虚构 DOM
简而言之,就是通过 JS 来模仿 DOM 构造,对于纠结以什么 JS 数据结构来模仿 DOM 并没有一套规范,只有能齐全笼罩 DOM 的所有构造即可,上面以较为通用的形式演示一下。
通过对 DOM 构造的剖析,咱们能够用 tag 示意 DOM 节点的类型,props 示意 DOM 节点的所有属性,包含 style、class 等,children 示意子节点(没有子节点则示意内容),这样子咱们就把整个 DOM 通过 JS 模仿进去了,而后呢? 而后看看下一章~~~
// DOM
<div class="container">
<h1 style="color: black;" class="title">HeiHei~~</h1>
<div class="inner-box">
<span class="myname">I am Yimwu</span>
</div>
</div>
// VDOM
let vdom = {
tag: 'div',
props: {classname: 'container',},
children: [
{
tag: 'h1',
props: {
classname: 'title',
style: {color: 'black'}
},
children: 'HeiHei~~'
},
{
tag: 'div',
props: {classname: 'inner-box',},
children: [
{
tag: 'span',
props: {classname: 'myname'},
children: 'I am Yimwu'
}
]
}
]
}
虚构 DOM 的作用
当咱们可能在 JS 中模拟出 DOM 构造后,咱们就能够通过 JS 来对 DOM 操作进行优化了,怎么优化呢,这个时候 diff 算法就该退场了。当咱们通过 JS 对 DOM 进行批改后,并不会间接触发 DOM 更新,而是会学生成一个新的虚构 DOM,而后利用 diff 算法与批改前生成的虚构 DOM 进行比拟,找出须要批改的点,最初进行真正的 DOM 更新操作
Vue 源码中的 diff 算法
patch.js 门路
Vue 中的 diff 算法相干代码次要在 patch.js 文件中,门路如下图
参考 前端 vue 面试题具体解答
patch 函数
1、如果新节点不存在 (vnode is undefined),间接执行 destroyhook 并返回
2、如果旧节点不存在 (oldVnode is undefined),间接创立新节点
3、如果新节点与旧节点都存在则进入下一层判断,对节点进行比对
4、应用 sameVnode 函数判断新节点与旧节点是否为雷同的节点,如果雷同则递进比照其子节点,如果不同则间接从新创立新节点
patchVnode 函数
1、如果新节点为文本节点 (isUndef(vnode.text) === false) 且 新旧节点文本不同(oldVnode.text !== vnode.text),则间接设置(setTextContent)元素(ele)的文本
2、如果新节点不是文本节点,则又分为以下几种状况
2.1、如果新节点和旧节点都有 child,则调用 updateChildren 更新子节点
2.2、如果只有新节点有 child,则间接增加子节点(addVnode)
2.3、如果只有旧节点有 child,则间接删除子节点(removeVnodes)
2.4、如果旧节点有 text,则删除 text(setTextContext)
updateChildren
updateChildren 函数采纳的是双端 diff,所谓双端,也就是从新旧节点的两端同时向两头比拟,比拟的步骤如下:
1、新开始节点 vs 旧开始节点,如果雷同则间接遍历其 children,调用 patchVnode 比拟子元素差别,指针往前走一步
2、新完结节点 vs 旧完结节点,如果雷同则间接遍历其 children,调用 patchVnode 比拟子元素差别,指针往前走一步
3、旧开始节点 vs 新完结节点,如果雷同则先把新完结节点挪动到旧开始节点的前一个地位,而后遍历其 children,调用 patchVnode 比拟子元素差别,指针往前走一步
4、旧完结节点 vs 新开始节点,如果雷同则先把新开始节点挪动到旧完结节点的后一个地位,而后遍历其 children,调用 patchVnode 比拟子元素差别,指针往前走一步
5、若后面 4 种状况都没有命中,则将遍历新节点,将子节点组个与旧节点的子节点进行一一比拟,一一遍历比照,没有匹配到的则间接重建元素
diff 算法中的 Key 值
从 diff 算法的 updateChildren 函数中咱们晓得,采纳双端 diff 算法会进行新的开始、完结节点和旧的开始、完结节点做比照,当都没有匹配上的时候会采纳齐全遍历的形式进行一一比拟,那么这个时候 key 就施展出作用了,当咱们从新的节点中遍历节点,拿去和旧节点匹配时,如果 key 匹配上的话,那么就表明该元素只是地位产生了挪动,间接调整地位后对其子节点进行(sameVnode)查看即可,而不须要齐全重建元素,大大节俭了性能。
v-for 中 key 值是否能够为 index
答案当然是不能够,举个例子,咱们来看上面两个 vdom,从 num 值咱们能够发现,新、旧两个 vdom 是两个程序相同的数组生成的 vdom,装置失常的形式,应该是简略调换一下程序,间接复用 3 个元素即可,而当咱们以 index 作为 key 时,状况就不同了,因为 index 永远都是从 0 开始,所以这两个 vdom 的 key 值从开始到完结,看起来都是雷同的,这就导致了当咱们去比照 key 值的时候会发现他们每个都是匹配的,而后对其子节点进行 patchVnode,这个时候因为 props 不同,即 num 不同,因而会触发对应的响应式值的更新机制,而且在这个过程中还会调用多个更新相干的钩子函数,如果定义的属性十分多的话,触发更新将会导致十分大的性能损耗,因而,在应用 v-for 的时候,倡议应用相似 id 这种惟一标识的字段代替 index,防止不必要的性能损耗!
const oldVdom = {
tag: "div",
children: [
{
tag: "div",
key: 0,
num: 1
},
{
tag: "div",
key: 1,
num: 2
},
{
tag: "div",
key: 2,
num: 3
},
]
}
const newVdom = {
tag: "div",
children: [
{
tag: "div",
key: 2,
num: 3
},
{
tag: "div",
key: 0,
num: 1
},
{
tag: "div",
key: 1,
num: 2
},
]
}
总结
对于 VDOM 以及 diff 算法的学习,领会到了前端对于性能的极致谋求,通过通读 vdom 源码,根本可能从更加粗浅的角度去了解采纳 VDOM 的目标,以及 key 值在 diff 算法中的真正作用,也可能从更加底层的角度了解为什么不举荐应用 index 作为 key 这个 Best Practices!