虚拟DOM和DOM diff

136 阅读4分钟

虚拟 DOM 是什么?

DOM 是很慢的。如果把一个简单的 div 元素的所有属性都打印出来,可以看到: 并且这只是第一层,真正的 DOM 元素非常庞大,因为标准就是如此。

相对于 DOM 对象,原生 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息都可以很容易的利用 Javascript 对象表示出来:

const element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

对应的HTML的写法是:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

DOM 树的信息都可以用 Javascript 对象来表示,就可根据这个用 Javascript 对象表示的树结构来构建一棵真正的DOM树。

虚拟DOM是相对真实DOM的一个概念。他实际是把DOM对象抽象起来成一个javascript对象。虚拟DOM是和新一代框架组件概念想对应的。在框架内部可以对虚拟DOM进行diff来达到优化的效果。

虚拟 DOM 的优点

减少 DOM 操作

  1. 减少 DOM 操作虚拟 DOM 可以将多次操作合并为一次操作,比如你添加 1000 个节点,却是一个接一个操作的(减少频率)

  2. 虚拟 DOM 借助 DOM diff 可以把多余的操作省掉,比如你添加 1000 个节点,其实只有 10 个是新增的(减少范围)

跨平台

虚拟 DOM 不仅可以变成 DOM,还可以变成小程序、iOS 应用、安卓应用,因为虚拟 DOM 本质上只是一个 JS 对象

虚拟 DOM 的缺点

  • 某些情况下,虚拟DOM的执行效率不如真实DOM操作
  • 需要额外的创建函数,如 createElement 或 h,但可以通过 JSX 来简化成 XML 写法

DOM diff 是什么

DOM diff 即比较两颗虚拟 DOM 树区别的算法,记录这两棵树差异,记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

DOM diff 可能的大概逻辑

  1. Tree diff
  • 将新旧两棵树逐层对比,找出哪些节点需要更新
  • 如果节点是组件就看 Component diff
  • 如果节点是标签就看 Element diff
  1. Component diff
  • 如果节点是组件,就先看组件类型
  • 类型不同直接替换(删除旧的)
  • 类型相同则只更新属性
  • 然后深入组件做 Tree diff(递归)
  1. Element diff
  • 如果节点是原生标签,则看标签名
  • 标签名不同直接替换,相同则只更新属性
  • 然后进入标签后代做 Tree diff(递归)

DOM diff的问题(key)

举个Vue的例子(代码),在3个 input 依次输入三角形、正方形和圆形

然后点击中间的 delete 按钮,你会看到结果:

令人疑惑的在于原来的第三项「圆形」消失了!「正方形」却被保留下来了!

原因

你认为你删除了2,但Vue会认为你做了两件事:

  • 把2变成了3
  • 然后把3删除了 Vue 为什么要舍近求远呢?看看这两个数组:[1,2,3] 和 [1,3],人类会说,这不就是少了个 2 吗?

但是计算机会怎么对比数组?遍历!

首先对比 1 和 1,发现「1 没变」;然后对比 2 和 3,发现「2 变成了 3」;最后对比 undefined 和 3,发现「3 被删除了」。

所以计算机的结论是:「2 变成了 3」以及「3 被删除了」,所以再看之前结果就很合理了。

破解之法 --- 用一个唯一id作为 key

为什么不能用 index 作为 key

如果你用 index 作为 key,那么在删除第二项的时候,index 就会从 1 2 3 变成 1 2(因为 index 永远都是连续的,所以不可能是 1 3),那么 Vue 依然会认为你删除的是第三项。也就是会遇到上面一样的 bug。