虚拟 DOM 与 DOM diff

204 阅读3分钟

关于DOM的谣言

  1. DOM操作慢?虚拟DOM快 DOM操作慢是对比于JS原生API,如数组操作 任何基于DOM的库(Vue/React)都不可能在操作DOM时比DOM快

  2. 为什么网上有这些谣言? 因为在某些情况下,虚拟DOM快,如下

虚拟DOM优点

减少DOM操作次数

虚拟DOM可以将多次操作合并为一次操作,比如你添加1000个节点,却是一个接一个操作,react和vue只需将1000个文本放数组里,一次性的添加到页面

减少DOM操作的范围

虚拟DOM借助DOM diff可以把多余的操作省掉,比如你添加1000个节点,其实只有10个是新增的,vue和react通过对比,发现有990个页面存在的,就只把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",  // 标签名 或者 组件名
}

vue

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

如何创建虚拟DOM

React.createElement

createElement('div', {className: 'red', onClock: ()=>{}}, [
  createElement('span', {}, 'span1'),
  createElement('span', {}, 'span2')
])

vue(只能在render函数里得到h)

h('div', {
  class: 'red',
  on: {
    onclick: ()=>{}
  }
}, [h('span', {}, 'span1'), h('span', {}, 'span2')])

上面创建虚拟DOM的方法很傻,使用JSX简化

现在创建虚拟DOM的写法:

React

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

通过 babel 转为 createElement 形式

vue

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

通过 vue-loader 转为 h 形式

总结

  1. 虚拟 DOM 是什么 一个能代表 DOM 树的对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性

  2. 虚拟 DOM 有什么优点 能减少不必要的 DOM 操作 能跨平台渲染

  3. 虚拟 DOM 有什么缺点 需要额外的创建函数,如 createElement 或 h,但可以通过 JSX 来简化成 XML 写法,但这种简化写法严重依赖打包工具

浏览器渲染时会让页面不可交互

DOM diff

虚拟 DOM 的对比算法

图示

把虚拟 DOM 想象成树形

<div :class="x">
    <span v-if="y">{string1}</span>
    <span>{string2}</span>
</div>

image.png

当数据变化时

  1. x 从 red 变成 green

image.png

image.png

DOM diff 发现

  • div 标签类型没变,只需要更新 div 对应的 DOM 的属性
  • 子元素没变,不更新
  1. y 从 true 变成 false

image.png

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 可能的大概逻辑

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

DOM diff 的缺点

[www.zhihu.com/question/61… Vue2.0 v-for 中 :key 到底有什么用?)

这个问题在 React 中也存在

juejin.im/post/684490…

zhuanlan.zhihu.com/p/20346379