虚拟DOM与DOM diff

101 阅读5分钟

虚拟DOM是什么

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

关于DOM的谣言

DOM操作慢?虚拟DOM快

  • 这句话类似于:刘翔矮(对比于姚明)

  • DOM操作慢是对比于JS原生API,如数组操作

  • 任何基于DOM的库(Vue/React)都不可能在操作DOM时比DOM快

为什么存在这些谣言?因为在某些情况下。虚拟DOM快

虚拟DOM优点

减少DOM操作(两个例子)

  • (减少DOM操作的次数) 虚拟DOM可以将多次操作合并为一次操作,比如你添加1000个节点,却是一个接一个操作的(DOM操作1000次,虚拟DOM只需要操作一次)(不是优化DOM操作,而是优化DOM操作的次数)

  • (减少DOM操作的范围) 虚拟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 组件名
    ...
  }

这个对象就表示了一个标签为div,子元素为两个span,className为red,点击事件调用函数的一个DOM

Vue

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

这个虚拟DOM, Vue里面表示div,class为red,onclick执行一个函数,子元素是两个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(现在创建虚拟DOM的方法)

React JSX

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

通过 babel转为 createElement 形式

为什么React可以用babel来转换?

因为React团队和babel团队关系很好,所以babel团队就把React JSX语法直接作为插件内置了。使用最新的配置就不需要再配置就可以支持。

Vue Template

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

通过 vue-loader转为 h 的形式

为什么vue不能用babel来转换?

因为两个团队互不认识。Vue使用的是单文件(.vue),它不是JS,不是原生的JS语法,不能用JS来理解.vue,只能通过vue-loader。而React默认的就是JS

虚拟DOM有什么缺点

  • 需要额外的创建函数,如createElement或h,但可以通过JSX来简化XML写法

  • React JSX 和 Vue Template两种方法严重依赖打包工具,不打包的话,是不认识标签的意思,需要额外的添加构件的过程。

DOM操作并不慢,只是渲染慢

image.png

JS用时是很快的,但是浏览器在渲染页面的时候,可让页面不可交互

DOM操作快还是React操作快?

结论:不确定,当规模小的时候,一定是React优化的更好,它会减少不必要的操作和bug;当规模到达很大的时候,它的额外的计算会使自己变慢。

结论1:当你的数据规模是合理的范围,只有几千的话,用虚拟的DOM是很好的,可以很好的优化多余的操作或者是没有优化的操作;当时当你的规模大到一定的范围,在这种极限的情况下,原生的DOM会保证一定的稳定性(Vue会默认的做一些优化)

DOM diff

虚拟DOM的对比算法

image.png

image.png

image.png

什么是DOM diff

  • 就是一个函数,我们称之为patch

  • patches = patch(oldVNode, newVNode)

  • patches就是要运行的DOM操作,可能长这样:

[
  {type: 'INSERT', vNode: ...},
  {type: 'TEXT', vNode: ...}
  {type: 'PROPS', propsPatch: [...]}
]

比对(diff) 渲染更新前后产生的两个虚拟dom对象的差异,并产出差异补丁对象,再将差异补丁对象应用到真实dom节点上

DOM diff可能的大概逻辑

Tree diff

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

Component diff

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

Element diff

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

DOM diff的缺点

  • 同级节点对比存在bug

DOM diff 中的key问题

key的作用主要是为了高效的更新虚拟DOM。

diff算法是从左往右进行同层级对比的,如果发现元素相同但是内容不相同,会直接修改内容。解决的方法就是加上唯一的 key,让 Diff 知道就算是同类型的组件,也是有名字区分的,更新视图就不会出错。

如果没有key值,就会根据就地复用的原则,一个一个对比,然后修改渲染,场景:在同一层级的某一堆节点中插入一个新节点。如果有key,diff算法就可以通过对比找到正确的位置插入新节点,而key值相同的dom节点就不要去比较。

如果key值用index,假如我在数组中间插入一项的时候,此时从这一项开始的key值就全部都变了,都需要重新对比渲染。因此,复杂的列表不要用index。

参考文档