虚拟 DOM 和 DOM diff

虚拟 DOM

与「真实 DOM」对应

关于 DOM 的谣言

DOM 操作慢?虚拟 DOM 快

  • 这句话类似于:刘翔矮(对比于姚明)
  • DOM 操作慢是对比于 JS 原生 API,如数组操作
  • 任何基于 DOM 的库(Vue/React)都不可能在操作 DOM 时比 DOM

为什么网上有这样的谣言?

  • 因为在某些情况下,虚拟 DOM

    哪些情况请继续看

虚拟 DOM 是什么

  • 是一个能代表 DOM 树的对象,通常含有标签名标签上的属性事件监听子元素们,以及其他属性

虚拟 DOM 的优点

减少 DOM 操作

  • 虚拟 DOM 可以将多次操作合并为一次操作,比如你添加 1000 个节点,却是一个接一个操作的(减少频率
  • 虚拟 DOM 借助 DOM diff 可以把多余的操作省掉,比如你添加 1000 个节点,其实只有 10 个是新增的(减少范围

跨平台

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

虚拟 DOM 长什么样子

  • React
const vNode = {
  key: null,
  props: {
    children: [  // 子元素们
       { type: 'span', ... },
       { type: 'span', ... }
    ],
    className: "red" // 标签上的属性
    onClick: () => {} // 事件
  },
  ref: null,
  type: "div", // 标签名 or 组件名
  ...
}
  • Vue
const vNode = {
  tag: "div", // 标签名 or 组件名
  data: {
    class: "red", // 标签上的属性
    on: {
      click: () => {} // 事件
    }
  },
  children: [ // 子元素们
    { tag: "span", ... },
    { tag: "span", ... }
  ],
  ...
}

如何创建虚拟 DOM

  • React.createElement
createElement("div", { className: "red", onClick: () => {} }, [
  createElement("span", {}, "span1"),
  createElement("span", {}, "span2"),
]);
  • Vue(只能在 render 函数里得到 h
h('div', {
  class: 'red',
  on: {
    click: () => { }
  },
}, [h('span',{},'span1'), h('span', {}, 'span2'])

用 JSX 简化创建虚拟 DOM

  • React JSX
<div className="red" onClick="{()=> {}}">
  <span>span1</span>
  <span>span2</span>
</div>
  • Vue Template
h('div', {
  class: 'red',
  on: {
    click: () => { }
  },
}, [h('span',{},'span1'), h('span', {}, 'span2'])

现在创建虚拟 DOM 的方法

  • React
<div className="red" onClick={fn}>
  <span>span1</span>
  <span>span2</span>
</div>

通过 babel 转为 createElement 形式

  • Vue Template
<div class="red" @click="fn">
  <span>span1</span>
  <span>span2</span>
</div>

通过 vue-loader 转为 h 形式

虚拟 DOM 的缺点

  • 需要额外的创建函数,如 createElementh,但可以通过 JSX 来简化成 XML 写法
  • 需要额外的转义构建工具
  • 节点数量少时虚拟 DOM 效率高,但是节点数量多时虚拟 DOM 性能比不上原生DOMVue接近原生DOMReact性能偏差)

DOM diff

虚拟 DOM 的对比算法

DOM diff 的图示

  • 把虚拟 DOM 想象成树形
<div :class="x">
    <span v-if="y">{string1}</span>
    <span>{string2}</span>
</div>

当数据变化时

  • xred

  • 变成 green

  • DOM diff 发现
    • div 标签类型没变,只需要更新 div 对应的 DOM 的属性
    • 子元素没变,不更新

当数据变化时

  • ytrue 变成 false

  • DOM diff 发现
    • div 没变,不用更新
    • 子元素1标签没变,但是children变了,更新 DOM 内容
    • 子元素2不见了,删除对应的 DOM

小结: 什么是 DOM diff

  • 就是一个函数,我们称之为 patch
  • patches = patch(oldVNode, newVNode)
  • patches 就是要运行的 DOM 操作,可能长这样:
[
  {type: 'INSERT', vNode: ... },
  {type: 'TEXT',  vNode: ... },
  {type: 'PROPS', propsPatch: [...]}
]

DOM diff 可能的大概逻辑

Tree diff

  • 将新旧两棵树逐层对比,找出哪些节点需要更新
  • 如果节点是组件就看 Component diff
  • 如果节点是标签就看 Element diff

Component diff

  • 如果节点是组件,就先看组件类型
  • 类型不同直接替换(删除旧的)
  • 类型相同则只更新属性
  • 然后深入组件做 Tree diff(递归)

Element diff

  • 如果节点是原生标签,则看标签名
  • 标签名不同直接替换,相同则只更新属性
  • 然后进入标签后代做 Tree diff(递归)

DOM diff 的优点

DOM diff 算法会对比 oldNodenewNode 的区别,从而减少不必要的渲染

DOM diff 的问题

  • DOM diff 在同层级对比中有 bug, 会造成页面渲染错误。
  • 同一层级的一组节点可以通过唯一的id进行区分, 所以可以给节点设定唯一的key,从而消除bug
  • key 只能是 number 和 string 类型,一定不要用 index 作为 key 值。

扩展阅读:

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

React 虚拟 Dom 和 diff 算法

React 源码剖析系列 - 不可思议的 react diff - 知乎

comments powered by Disqus