V-for就地复用、虚拟DOM、Diff算法

V-for 就地复用原理


举个 🌰:

1
2
3
4
5
<div v-for="(item,index) in items">
  <input />
  <button @click="del(index)">delthis</button>
  {{item.message}}
</div>

JS 部分

1
2
3
4
5
6
7
8
9
10
11
//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 性能差”,这是因为 ——

  1. DOM 引擎JS 引擎相互独立,但又工作在同一线程(主线程),因此 JS 代码调用DOM API时必须挂起 JS 引擎、激活 DOM 引擎,完成后再转换到 JS 引擎
  2. 引擎间切换的代价会迅速积累
  3. 强制重排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 算法的两个核心:

  1. 两个相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构。

  2. 同一层级的一组节点,他们可以通过唯一的 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 元素(右边)

图片

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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>标签绑定在一起:

1
<p>Hello {{ name }}</p>

让我们梳理一下 vue 对这个节点 p 和数据所做的一切

  • Vue 会把这些模板编译成一个渲染函数 render
  • 该函数被调用后会渲染并且返回一个虚拟的 DOM 树. 这个 “树” 的职责就是描述当前视图应处的状态。
  • 之后再通过一个Patch 函数,计算和旧虚拟 dom 树的差集,并通过打补丁的方式将差集中的虚拟节点更新到真实 DOM 树。
  • 在整个过程中, Vue 借助数据劫持和订阅者模式实现监听状态、依赖收集、依赖追踪通知变动等。 会侦测在渲染过程中所依赖到的数据来源,以实现双向绑定,自动更新状态。

参考:

了解一下 v-for 原理

Vue2.0 v-for 中 :key 到底有什么用?

Vue–patch | 学 Vue 看这个就够了

https://www.zhihu.com/question/324992717/answer/690011952

vue 考点 —— Diff 算法

既然用 virtual dom 可以提高性能,为什么浏览器不直接自带这个功能呢?–水歌 | 知乎

你不知道的 React 和 Vue 的 20 个区别【面试必备】

vue diff 算法 patch

diff 算法中的概念

cookie和session和localstorage 由arguments属性浅谈JS函数重载与多态性

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×