虚拟DOM本质上是一个JavaScript对象。用对象的形式把DOM结构描述出来。以一个简单的 HTML 片段
为例,在虚拟 DOM 中,它会被转化为类似如下的 JavaScript 对象结构:
{
tag: 'div',
attrs: { id: 'container' },
children: [
{
tag:'span',
attrs: { class: 'text' },
children: ['Hello']
}
]
}
在Vue里,代码在渲染到页面之前,会先被转成虚拟DOM对象。它用这个对象来描述真实DOM的结构,最终再根据这个对象渲染到页面上。每次数据发生变化前,Vue都会缓存一份当时的虚拟DOM。一旦数据有变化,新生成的虚拟DOM就会和缓存的虚拟DOM进行比较。Vue内部封装了一个叫diff的算法,这个算法会仔细对比两个虚拟DOM,只找出那些有变化的部分。渲染的时候,就只修改这些发生变化的地方,原来没变化的部分还用原来的数据渲染,这样就能高效地更新页面,又不会浪费性能在没变化的地方。
现代前端框架基本都要求不用手动操作DOM。一方面,手动操作DOM很难保证程序性能。在多人一起开发的项目里,如果代码审查不严格,有的开发者可能会写出让性能变差的DOM操作代码。另一方面,更关键的是,不手动操作DOM能极大地提高开发效率。有了虚拟DOM,开发者只要关注数据和业务逻辑,框架会自动根据数据变化通过虚拟DOM高效地更新页面,不用再去操心复杂又易错的DOM操作细节了 。
虚拟 DOM 是现代前端框架中用于优化页面渲染性能的重要概念,其解析过程主要包含以下几个关键步骤:
在前端应用初始化或组件首次渲染时,会对即将插入到文档中的 DOM 树结构进行分析。具体来说,会使用 JavaScript 对象来表示真实的 DOM 结构。例如,对于一个简单的 HTML 片段 Hello, World!
,对应的虚拟 DOM 对象可能如下所示:
{
tag: 'div', // 标签名(TagName)
props: { id: 'box', class: 'container' }, // 元素属性(props)
children: [
{
tag: 'p',
props: {},
children: ['Hello, World!'] // 子元素或文本内容(Children)
}
]
}
通过这种方式,将整个 DOM 树转化为一个 JavaScript 对象树,这个对象树就是虚拟 DOM。然后,会将这个虚拟 DOM 对象树保存下来,以便后续进行对比和更新操作。
当页面的状态发生改变,导致需要对页面的 DOM 结构进行调整时:
元素的文本内容变为了 Hello, Vue!
,那么新的虚拟 DOM 对象树中对应的部分会更新为:{
tag: 'div',
props: { id: 'box', class: 'container' },
children: [
{
tag: 'p',
props: {},
children: ['Hello, Vue!']
}
]
}
虚拟 DOM 通过将真实 DOM 抽象为 JavaScript 对象树,在状态变更时进行高效的对比和更新,从而减少了直接操作真实 DOM 的次数,提高了页面的渲染性能和响应速度。
虚拟DOM本质上就是JavaScript对象。这个特性让它在不同平台间操作变得很方便。比如说在服务端渲染(SSR)中,服务器需要生成HTML页面发送给浏览器。传统的直接操作真实DOM在服务器环境里很难实现,因为服务器没有浏览器那样的图形界面和DOM环境。但虚拟DOM就可以在服务器端用JavaScript轻松处理,生成HTML片段后再发给浏览器。还有像uniapp这样的跨平台开发框架,通过虚拟DOM,一套代码可以同时生成适配不同平台(如微信小程序、H5页面、APP等)的界面。因为不同平台对界面的渲染方式有差异,虚拟DOM作为中间层,能把JavaScript层面的界面描述,转化为各个平台所需要的原生组件或视图,大大提高了开发效率,减少了为不同平台单独开发代码的工作量 。
在现代前端框架中,Diff算法是虚拟DOM实现高效更新的关键技术。当新的虚拟DOM树和旧的虚拟DOM树进行对比时,Diff算法主要遵循以下步骤和规则:
首先,Diff算法会对比新老虚拟DOM中的节点本身,判断它们是否为同一节点。这里判断同一节点的依据通常是节点的类型(比如是 当判断两个节点为相同节点时,就会进入 Diff算法只对同层的子节点进行比较,放弃跨级的节点比较。这是因为如果对所有节点都进行递归比较(包括跨级比较),时间复杂度会达到 O ( n 3 ) O(n^3) O(n3),这是非常高的计算成本。而通过只进行同层比较,Diff算法将时间复杂度降低到了 O ( n ) O(n) O(n)。也就是说,只有当新老虚拟DOM的子节点列表都包含多个子节点时,才会使用上述核心的Diff算法进行同层级的比较和处理。 通过以上这些步骤和规则,Diff算法能够高效地找出新老虚拟DOM之间的差异,并将这些差异应用到真实DOM上,从而实现页面的快速更新,减少不必要的DOM操作,提升前端应用的性能。 vue 中 key 值的作用可以分为两种情况来考虑: 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。 想象你有一个页面,上面有个登录框和注册框,通过点击按钮切换显示。代码可能像这样: 这里两个 解决办法就是给 加上 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。 假设有一个待办事项列表,你可以点击按钮把完成的事项移到列表底部。代码如下: 这里用 正确做法是用每个事项的唯一标识作为 这样,Vue 就能根据 总结来说,在 v-if 中,key 能防止相同类型元素被错误复用,保证元素独立性;在 v-for 中,key 帮助 Vue 准确高效地更新虚拟 DOM,确保列表元素顺序和状态正确。 使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2…这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。 如果数据列表没id只能用index区分,可尝试: 等标签名)以及节点的唯一标识(如果设置了
key
属性,key
也会作为判断的重要依据)。如果两个节点的类型不同,那么它们被认为不是同一节点。此时,旧的节点会被直接删除,然后重新创建新的节点来进行替换。例如,旧虚拟DOM中有一个 节点,那么Diff算法会删除原来的
节点。
相同节点的处理(patchVnode)
patchVnode
阶段,这个阶段主要处理该节点的子节点情况:
节点且包含多个 子节点,新虚拟DOM中对应的
节点没有子节点了,那么Diff算法会把页面上原来的那些 子节点都删除掉。
updateChildren
。在这个过程中,算法会对新老节点的子节点进行操作。具体来说,它会从两端开始向中间进行比较(也称为双端比较)。
key
判断),如果相同,则递归地比较这两个子节点的子节点(也就是继续深入比较它们的下一层子节点)。key
进行查找),如果找到,就将其移动到合适的位置,然后继续进行比较。同层比较原则
6、Vue中key的作用
v-if 中 key 的作用
form
都是表单元素。当你从登录框切换到注册框,再切回来时,Vue 会复用表单元素。结果就是,切换回登录框时,用户名输入框可能还保留着注册时输入的新用户名,这不是我们想要的。form
加上 key
:<template>
<div>
<button @click="showLogin =!showLogin">切换button>
<form v-if="showLogin" key="login-form">
<input type="text" placeholder="用户名">
<input type="password" placeholder="密码">
form>
<form v-else key="register-form">
<input type="text" placeholder="新用户名">
<input type="password" placeholder="新密码">
<input type="password" placeholder="确认密码">
form>
div>
template>
<script>
export default {
data() {
return {
showLogin: true
};
}
};
script>
key
后,Vue 就知道这是两个完全不同的表单,不会复用,每次切换都能保持各自输入框的状态正确。v-for 中 key 的作用
index
作为 key
。当你点击按钮,Vue 会复用 li
元素,不会按新顺序重新排列。结果可能是已完成的事项虽然在数据里移到了最后,但在页面上显示还是乱的。key
,比如给每个待办事项加个 id
:<template>
<div>
<button @click="moveDoneToBottom">完成的移到最后button>
<ul>
<li v-for="item in todos" :key="item.id">
{{ item.text }} - {{ item.done? '已完成' : '未完成' }}
li>
ul>
div>
template>
<script>
export default {
data() {
return {
todos: [
{ id: 1, text: '买菜', done: false },
{ id: 2, text: '做饭', done: true },
{ id: 3, text: '洗碗', done: false }
]
};
},
methods: {
moveDoneToBottom() {
this.todos = this.todos.filter(item =>!item.done)
.concat(this.todos.filter(item => item.done));
}
}
};
script>
key
准确跟踪每个 li
元素,按新顺序更新页面,让显示和数据一致。
7、为什么不建议用index作为key?
tempId
的临时唯一属性,如item.tempId =
temp_${index}`` ,再用其作key
。name
和category
等属性组合唯一,可将其组合成key
,如``:key=“${item.name}_${item.category}
”` 。let uniqueIdCounter = 0; function generateUniqueId() { return
unique_${uniqueIdCounter++}; }
,为数据项添加生成的标识作key
。