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

Crushdada

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 性能差”,这是因为 ——

  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元素(右边)

实现如下:

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 借助数据劫持和订阅者模式实现监听状态、依赖收集、依赖追踪通知变动等。 会侦测在渲染过程中所依赖到的数据来源,以实现双向绑定,自动更新状态。

参考:

了解一下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算法中的概念

阅读 721
116 声望
3 粉丝
0 条评论
你知道吗?

116 声望
3 粉丝
文章目录
宣传栏