V-for就地复用原理
举个🌰:
<div v-for="(item,index) in items">
<input />
<button @click="del(index)">delthis</button>
{{item.message}}
</div>
JS部分
//data里面的items
items: [
{ message: "1" },
{ message: "2" },
{ message: "3" },
{ message: "4" },
],
//methods中的del方法
del(index) {
this.items.splice(index, 1); //根据传入的index删掉items中对应数据
},
效果如下
可以发现:
- 当删掉items中的第二个对象时,输入框中的值还是2--这意味着没有删除对应的第二个节点。这是因为vue采用虚拟DOM+diff算法导致的数据混乱。
vue监听到items数组中少了个元素后,会更新虚拟DOM,然后使用diff算法比较新、旧DOM树,在这个过程中,由于要计算出真实DOM树的最小变更规模,因此会尽可能复用已有的节点(如果节点类型相同)
此处,我们的需求当然是不复用节点,那该如何实现呢?
:key解决v-for导致的数据混乱
- 在渲染列表时,为每个元素绑定独一无二的key,这样,vue在更新经v-for渲染过的列表时,由于key值不同,会认为是不同的节点类型,不采取复用。这样就避免了数据混乱
为什么不能使用数组下标作为key:
不能使用各元素的index作为key,因为当新增或删除列表中元素时,各项索引都会变,也就是说索引对应元素变了,失去了标识的唯一性
声明式渲染
Vue 提供一套基于 HTML 的模板语法,允许开发者声明式地将真实 DOM 与 Vue 实例的数据绑定在一起。
"声明式" 的意思就是: 只需要指出目标, 而不用关心如何实现,将实现交由vue处理
虚拟DOM
Vdom(virtual dom),可以看作是一个使用javascript模拟了DOM结构的树形结构
- 其中Vnode节点对应真实DOM节点
Vdom树用于缓存真实DOM树的所有信息
为什么要采用虚拟DOM?
一切为了性能。
“直接操作 DOM 性能差”,这是因为 ——
- DOM 引擎、JS 引擎相互独立,但又工作在同一线程(主线程),因此JS 代码调用DOM API时必须挂起 JS 引擎、激活 DOM 引擎,完成后再转换到 JS 引擎
- 引擎间切换的代价会迅速积累
- 强制重排的DOM API调用,哪怕只改动一个节点,也会引起整个DOM树重排,重新计算布局、重新绘制图像会引起更大的性能消耗
所以,降低引擎切换频率(减少DOM操作次数)、减小 DOM 变更规模才是DOM 性能优化的两个关键点。
虚拟 DOM +diff算法是一种可选的解决方案
基本思路:“在 JS 中缓存必要数据,计算界面更新时的数据差异,只提交最终差集”。
- 虚拟dom只用于缓存,而
diff算法负责--
- 计算出‘虚拟dom和目前真实DOM之间的数据差异’
- 提交最终差集
注意:“单纯VDOM是提高不了性能的,VDOM主要作用在于它的二次抽象提供了一个diff/patch和batch commit(批量提交)的机会”
watcher的节流效果:借助watcher响应式原理,使数据异步更新(滞后更新),能够实现节流效果,在一段时间内,允许多次更新虚拟DOM,然后一次性patch到真实DOM树。像是使用精灵图以减少请求次数那样,达到优化性能的目的。
vue在监听到数据变动后,会将依赖该数据的watcher加入微任务队列,由于微任务是异步的,因此所有同步更新数据的操作,都会及时地在微任务队列中的任务更新前触发watcher响应,换个说法:执行第一次变动后的每次变动都会更新watcher中的各项依赖。这样的话,在该微任务执行完毕之前的这段时间,就相当于节流中的时延了
Vdom的Diff算法
diff算法的两个核心:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
- 同一层级的一组节点,他们可以通过唯一的key进行区分。
diff算法的复杂度
- 比较两棵虚拟DOM树的差异是Virtual DOM算法最核心的部分,这也是所谓的 VirtualDOM的diff 算法。两个树的完全的diff 算法是一个时间复杂度为O(n^3)的问题。
但是在前端当中,你很少会跨越层级地移动DOM元素。所diff算法只会对同一个层级的元素进行对比。下面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到O(n)。
比较时能否复用的逻辑
当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
- 如果节点类型不同,直接干掉旧的节点,创建并插入新的那个节点,不会再比较这个节点以后的子节点了。
如果节点类型相同,则会直接复用该节点,重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。
比如--我们希望可以在B和C之间加一个F
Diff算法默认执行起来是这样的:
- 老的Vdom树的该层上有6个节点,新的Vdom树上有7个类型相同的节点,那么就依次复用真实DOM树该层上的对应的前6个节点,在最后再新建一个节点,赋予之前节点E的属性。
- 即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,这样vue会把他们当做是不同的节点,因此不会复用,diff算法会直接创建新的节点,并插入正确的位置
key的作用
- key的作用主要是为了高效的更新虚拟DOM。
- 也可避免直接复用v-for出来的节点,避免数据混乱
- 另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
patch到真实DOM
模拟实现
如何将vnode(左边)变成真实的DOM元素(右边)
实现如下:
let nodes = {
tag: "ul",
attrs: {
id: "list",
},
children: [
{
tag: "li",
attrs: {
class: "item",
},
children: ["Item 1"],
},
],
};
//实现方法:递归遍历
function createElement(vnode) {
var tag = vnode.tag;
var attrs = vnode.attrs || {};
var children = vnode.children || [];
if (!tag) {
return null;
}
var elem = document.createElement(tag);
var attrName;
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
for (let i = 0; i < children.length; i++) {
let childVnode = children[i];
if (typeof childVnode === "object" ||
childVnode.constructor === Object
) {
elem.appendChild(createElement(childVnode));
} else {
let text = document.createTextNode(childVnode);
elem.appendChild(text);
break;
}
}
return elem;
}
let elem = createElement(nodes);
console.log(elem);
PS:
vue 在patch时,在一个update 方法里面调用createElment()方法,通过虚拟节点创建真实的 DOM 并插入到它的父节点中;
相当于打补丁到真实DOM
🌰
最后,举个栗子梳理一下:
让 Vue 将name
的数据和<p>
标签绑定在一起:
<p>Hello {{ name }}</p>
让我们梳理一下vue对这个节点p和数据所做的一切
- Vue 会把这些模板编译成一个渲染函数render。
- 该函数被调用后会渲染并且返回一个虚拟的 DOM 树. 这个 "树" 的职责就是描述当前视图应处的状态。
- 之后再通过一个Patch 函数,计算和旧虚拟dom树的差集,并通过打补丁的方式将差集中的虚拟节点更新到真实 DOM树。
- 在整个过程中, Vue 借助数据劫持和订阅者模式实现监听状态、依赖收集、依赖追踪通知变动等。 会侦测在渲染过程中所依赖到的数据来源,以实现双向绑定,自动更新状态。
参考:
https://www.zhihu.com/question/324992717/answer/690011952
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。