解释:对比新旧虚拟DOM树,完成对真实DOM的更新,这个对比差异的过程叫做 diff。
Vue 会在内部的 patch
函数中完成该过程。
当组件创建时,或依赖的数据变化时,会运行一个特定的函数来做2件事:
_render
函数生成新的 VNode tree(虚拟DOM树)_update
函数,传入新的 VNode tree 的根节点,对比新旧2个树,最终完成对真实DOM的更新。代码表示大致逻辑:
// vue构造函数
function Vue(){
// ... 其他代码
var updateComponent = () => {
this._update(this._render())
}
new Watcher(updateComponent);
// ... 其他代码
}
diff 就发生在_update
函数的运行过程中。
Watcher
的作用:简单来说,运行传入的函数(updateComponent
),对函数中用到的响应式数据进行依赖收集。
Watcher
的作用具体参考Vue2-数据响应式原理
_update
函数接收一个 VNode
参数,也就是this._render()
返回的新生成的虚拟 DOM 树。
_update
函数通过当前组件的 this._vnode
属性,拿到旧的虚拟 DOM 树。
_update
函数首先会给组件的 this._vnode
属性重新赋值,让它指向新树。再判断旧树是否存在:
patch
函数直接遍历新树,为每个节点生成真实的DOM,并挂载到每个节点的 elm
属性上。(虚拟节点通过 elm
属性指向绑定的真实DOM。)patch
函数对新旧树对比,来实现2个目标:
不存在的流程:
存在时的流程:
// 伪代码表示:
function update(vnode) {
vnode // 新
this._vnode // 旧
this._vnode = vnode
}
这样就完成了组件的虚拟DOM树的更新。
但还需要解决真实的 DOM 更新(如果不考虑效率,直接用新树生成真实DOM即可)。而为了提升效率,需要对比新旧树,通过实现下面2个目标来提升效率。这个步骤在 _patch
函数中实现。
先来介绍几个术语,方便后续阅读:
tag
)类型、key
值均相同。input
元素还需要考虑 type
属性。不考虑内容,或后代节点。
<!-- 举例 -->
<!-- 节点相同 -->
<h1>123</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<h1>456</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<!-- 节点相同 -->
没有标签包裹的文字1 <!-- 对应节点 { tag: undefined , key: undefined } -->
没有标签包裹的文字2 <!-- 对应节点 { tag: undefined , key: undefined } -->
<!-- 节点相同 -->
<h1>123</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<h1>456</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<!-- 节点不同 -->
<input type="text" key="_key1"> <!-- 对应节点 { tag: input, key: _key1, data: {attrs: {type: text}} } -->
<input type="radio" key="_key1"> <!-- 对应节点 { tag: input, key: _key1, data: {attrs: {type: radio}} } -->
【新建元素】:根据一个虚拟节点提供的信息,创建一个真实的 DOM 元素,同时挂载到虚拟节点的 elm
属性上。
【销毁元素】:运行 vnode.elm.remove()
【更新】:2个虚拟节点进行对比更新,仅发生在2个虚拟节点【相同】的情况下。
【对比子节点】:对2个虚拟节点的子节点进行对比。
首先会对根节点比较,如果2个虚拟节点
【相同】:进入【更新】流程
newVNode.elm = oldVNode.elm
不【相同】:新节点递归的【新建元素】。旧节点直接【销毁元素】。
如果根节点都不相同,则没有对比的必要,直接当做旧树不存在处理。
diff 的重点。
再说明下 diff 的目的:为了修改真实的 DOM,并和新的 VNode tree 对应上。
在【对比】子节点时,vue 的实现思路:
实现大致逻辑:使用头尾指针+遍历来实现。动图演示(数字代表的是 key,蓝块中的数字代表真实DOM的内容):
注意,每个新旧节点【更新】时,都会递归的遍历子节点。
for 循环中的 item 如果不使用 key
,数据更新(尤其是位置发生了变化)后做 diff 时,会认为原来位置新旧头指针每次指向的虚拟节点都【相同】,则每个节点都会【更新】。如果子节点较多,效率就更低了。
举例:
<template>
<div>
<ul>
<li v-for="item in arr" :key="item">{{ item }}</li>
</ul>
<button @click="arr.unshift(99 + count++)">头部插入</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: [1, 2, 3, 4, 5],
count: 0,
};
},
};
</script>
因为 key
的存在,
加 key
的效果:
不加 key
的效果:
注意,
v-if/v-else
关于key
的问题,vue3 会自动添加,可以看这篇文章对比 vue2 和 vue3 的变化。
<template>
<div>
<div>
<span @click="isAccoutLogin = true">账号登录</span>
<span>|</span>
<span @click="isAccoutLogin = false">手机号登录</span>
</div>
<div v-if="isAccoutLogin" key="1">
<label>账号</label>
<input type="text" />
</div>
<div v-else key="2">
<label>手机号</label>
<input type="text" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
isAccoutLogin: true,
};
},
};
</script>
不加 key
时,因为节点相同,子节点也相同,所以不做更新。
加 key
才会有所区分,而清空输入框。
以上。