浅析 虚拟 DOM 和 DOM diff

148 阅读4分钟

1、虚拟 DOM

虚拟 DOM 是相对浏览器渲染出来的真实DOM而言的。虚拟DOM是一个 JS 对象,一个能代表DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素等。

以 Vue 的虚拟DOM为例:

const vNode = {
  tag: "div", // 标签名 or 组件名
  data: {
    class: "red", // 标签上的属性
    on: {
      click: () => {} // 事件
    }
  },
  children: [ // 子元素们
    { tag: "span", ... },    
  ],
  ...
}

在上面Vue的虚拟DOM例子里:tag用来表示标签名或组件名;class用来表示标签上的类;children来表示标签上的子元素。同理,在React的虚拟DOM里也有类似的描述真实DOM结构的属性。

1.1 虚拟DOM的优点

虚拟DOM与直接操作真实DOM相比有以下两个优点:

  • 减少了DOM操作。 虚拟DOM可以将多次操作合并为一次操作。比如虚拟DOM添加1000个节点,可以一次就添加进入真实DOM内;虚拟DOM借助 DOM diff 可以把多余的操作省掉。比如添加1000个节点,其实只有10个是新增的,那只需要进行新增10个节点的操作即可。
  • 跨平台。 由于虚拟DOM本质上只是一个 JS 对象,并不依赖真实平台环境,所以也可以应用在小程序、iOS应用、安卓应用等。

1.2 虚拟DOM的缺点

在创建虚拟DOM时,React需要通过createElement函数创建虚拟DOM,后来 React 使用了JSX语法来优化虚拟DOM的使用。 React创建虚拟DOM的方法改变如下:

//原来 React createElement
createElement('div',{className:'red',onClick:()=> {}},[
    createElement('span', {}, 'span1'),
    createElement('span', {}, 'span2')
  ]
)
//React 使用了JSX语法后
<div className="red" onClick={fn}>
    <span>span1</span>
    <span>span2</span>
</div>
//但是需要通过 babel 转为 createElement 形式

如上例所示,React 现在创建虚拟DOM是使用 JSX语法,并通过 babel 转为 createElement 形式。

而Vue则需要通过 render函数里得到的 h 来创建虚拟DOM,也可以简化通过采用 vue-loader 来转为h形式。如下例:

// Vue 直接通过render函数里得到的 h
h('div', {
  class: 'red',
  on: {
    click: () => { }
  },
}, [h('span',{},'span1'), h('span', {}, 'span2'])
//简化,采用 vue-loader 来转为h形式
//Vue Template
<div class="red" @click="fn">
  <span>span1</span>
  <span>span2</span>
</div>

原来创建虚拟DOM需要额外的函数:createElement 或 h;现在简化后可以通过 JSX 语法或 vue-loader来创建虚拟DOM,但是后者也有缺点,因为使用了JSX 语法或 vue-loader,使得虚拟DOM的创建又需要依赖打包了。

2、DOM diff

DOM diff从字面上可以理解就是比较两个DOM树的差异。

DOM diff 在做比较时分为了tree diff、component diff、element diff三个层级,分别描述三个层级大概的更新逻辑:

  1. Tree diff 逐层对比新旧两课树,找出需要更新的节点。如果节点是组件就看 component diff ;如果节点是标签就看 element diff

  2. component diff

  • 如果需要更新的节点是组件,就先观察组件类型;
  • 类型不同就直接替换,删除旧节点,后装载新节点。
  • 节点类型一样,仅仅是属性或者属性值变了。不会触发删除节点,只是会更新属性。
  • 最后深入组件再次进行 tree diff,逐层比较更深处的节点。
  1. element diff
  • 如果节点是原生标签,观察标签名;
  • 标签名不同,直接替换;
  • 标签名相同(即元素相同),就进一步比较更新属性。
  • 最后进入标签再次 tree diff,逐层比较更深处的节点。

3、DOM diff 优点和问题

关于 DOM diff 的优点,前面讲到了虚拟DOM减少DOM的操作的两种情况:

  • 虚拟DOM可以将多次操作合并为一次操作;
  • 虚拟DOM借助 DOM diff 可以省掉多余的操作。 以上的两种情况都需要通过 DOM diff 来对DOM树进行比较,记录之间存在的差异,然后再应用到真实的DOM上。

diff 的问题和 key

DOM diff 有时候会因为无法正确识别节点而进行多余的操作,

如下有A、B、C三个节点,希望在B、C节点之间插入一个E节点;

image.png 但是 Diff算法默认执行起来是A、B不变,C更新为E,最后插入C。

image.png 为了更高效的更新虚拟DOM,可以在 React/Vue 通过 key 来为每一个节点做一个唯一标识。比如说,上述的例子中如果为节点都设置了key ,DOM diff就能判断出来是新增了E节点,从而可以省略C更新E的操作。

使用 key 还可以管理可复用的元素。比如在Vue中如果有两个相同的元素,只需为两个标签添加一个具有唯一值的 key 即可表达出“这两个元素相互独立”。

注意:不要使用 index 作为key。因为 index 是连续的,以上面A、B、C节点为例,如果删除B节点,index会从1、2、3变成 1 2,被删除的会是C而不是B。