理解虚拟DOM机制和key属性的作用
什么是Vue的虚拟DMO?
虚拟DOM是区别于真实的DOM提出的。
在js事件直接操作DOM的时代(包括Jquery的时代),我们通过JS直接对真实的DOM树进行增删改查。
但是JS事件直接操作DOM会随着项目规模的扩大、事件的增加导致事件的管理以及事件和DOM之间的关系的维护变得日益复杂。
为了解决这个问题,以Vue为代表的新型前端框架(包括react等)提出了引入数据中间层来避免直接操作DOM的思路:让前端框架底层代替用户去操作DOM,用户不再关注DOM元素,而聚焦于数据(state)。使用Vue等框架,我们只需要修改数据,数据变化后Vue帮助我们来更新DOM。
但是DOM的变化是非常消耗计算机资源的,如何尽量的减少DOM的更新,是Vue需要考虑的。于是虚拟DOM的概念随之被提出。所谓的虚拟DOM并不是一个真正的DOM树,它是Vue底层在检测到数据修改后,不立刻直接去修改真实的DOM,而是结合数据和模板,生成一个临时的由js对象模拟的DOM树。
Vue的实际DOM树与虚拟DOM树比较算法
获得虚拟DOM之后,Vue底层会通过算法计算真实DOM树与虚拟DOM树的区别,并得到需要更新的节点,尽可能的复用现成的DOM节点,而不是去更新全部的DOM树,从而达到减少计算资源消耗的目的。
在实际项目中,DOM树可能极其复杂。为了提升真实DOM树和虚拟DOM树之间的比较效率,Vue提出了同层级比较的算法。即每次只比较处于同一个层级的DOM元素的变化情况。不同层级的不予比较。如下图所示就是只比较相同颜色区域是否发生变化。
那么Vue的同层比较算法具体是什么判断和处理逻辑呢?
它的基本逻辑是:当同一层的DOM节点中,如果能够判断节点的唯一性,那么尽可能的采用移动,插入等逻辑保持复用。如果不能保证唯一性,那么则采用更新,删除等操作实现目的。
Vue算法实现举例:
-
如下图这种情况(同一层级,不同的元素——元素类型唯一)
Vue能够根据元素类型判断BCD为三个不同的唯一的元素,故而采取了性能高消耗少的移动逻辑。即将BCD的顺序直接调整为CDB。全部复用了所用的DOM。 -
如下图这种情况(不同层级,不同元素)
Vue虽然能够根据元素类型判断BCD为三个不同的唯一元素,但是Vue的算法是同层比较,当Vue扫描时发现C节点不存在了,于是直接对C节点进行了删除,包括c节点下原有的EF。当递归到下一个层级时再创建新的c节点和ef节点。即BCD变成BD,Vue并不是把C移动到B下面,而是删除原有的C,重新在B下面新建了C及其下级的EF。没有复用C,E,F -
如下图这种情况(DOM的同层级,元素类型内容变化)
Vue检测到同一层不再存在C而是存在G,于是算法删除了C包括其下属的EF,新建了G;当递归到下一层级时,再为G创建了EF。没有复用EF。 -
如下图这种情况(DOM同层级,相同的元素——无法判断元素的唯一性)
这种情况下Vue无法通过元素类型判断元素的唯一,也没有key属性帮助其判断元素唯一,故而Vue认为元素不是唯一的。此时它会首先将B1更新成B2,再将B2更新成B1,并删除原B2下的EF。当递归到下一层级时,为新的B2新建EF。此种情况Vue无法复用EF。 -
如下图这种情况(DOM同类型节点的同层级顺序变化,有Key属性的情况下)。
当有key存在时,Vue底层能够判断节点的唯一性,故而Vue是采取的将B2节点包括下属的EF节点移动到B1之前的逻辑。完全复用了B2,E,F -
如下图这种情况。(同层级,新增元素,无key)
因为vue无法确定元素的唯一性,故而vue认为用户是想要更新节点。因此它会先将B2更新成B4,将B3更新成B2,最后新增一个B3.无法复用B2,B3 -
如下图这种情况。(同层级,新增元素,有key)
Vue通过key确定唯一性,会将b4直接插入到b1和b2之间,达到复用B2,B3的目的。
App.vue的唯一性改造
通过上述7种情况的描述,我们发现如果没有为Vue指定key属性,那么Vue在操作DOM时的效率,不会达到最优。
回到我们的todoitem组件,我们之前在组件中绑定了Key属性,但是我们的key属性绑定的是title,现在看来很不严谨,因为title极有可能出现重复,而一旦出现重复,vue在操作DOM的时候就有可能会采用消耗较大的处理逻辑。
<template>
<div id="app">
<input type="text" v-model="message"/>
<input type="text" :value="message" @input="handleChange"/>
{{message}}
<todolist>
<todoitem v-on:delete="handleDelete" v-model="Checkedmsg" v-for="item in list" :key="item.title" data-wen="wen" :title="item.title" :del="item.del">
<template v-slot:pretext="{val}">
<label>前置文字{{val}}</label>
</template>
</todoitem>
</todolist>
</div>
</template>
我们可以考虑将此处的key绑定为v-for的循环下标
<template>
<div id="app">
<input type="text" v-model="message"/>
<input type="text" :value="message" @input="handleChange"/>
{{message}}
<todolist>
<todoitem v-on:delete="handleDelete" v-model="Checkedmsg" v-for="(item,index) in list" :key="index" data-wen="wen" :title="item.title" :del="item.del">
<template v-slot:pretext="{val}">
<label>前置文字{{val}}</label>
</template>
</todoitem>
</todolist>
</div>
</template>
当然如果进一步考虑,假设list是一个一直在不断的被增删改查,排除的列表使用list下标也是不严谨的,最佳办法则是为组件的数据增加id返回值。
data(){
return{
message:"hello world",
Checkedmsg:false,
list: [
{
title: "新课程1",
del: false,
id:1
},
{
title: "新课程2",
del: true,
id:2
},
{
title: "新课程3",
del: false,
id:3
}
]
};
},
模板中使用id绑定key
<template>
<div id="app">
<input type="text" v-model="message"/>
<input type="text" :value="message" @input="handleChange"/>
{{message}}
<todolist>
<todoitem v-on:delete="handleDelete" v-model="Checkedmsg" v-for="item in list" :key="item.id" data-wen="wen" :title="item.title" :del="item.del">
<template v-slot:pretext="{val}">
<label>前置文字{{val}}</label>
</template>
</todoitem>
</todolist>
</div>
</template>