虚拟DOM和DOM Diff

129 阅读4分钟

虚拟DOM

1 什么是虚拟DOM

虚拟DOM是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,通过渲染过程的操作将其转化为真实的DOM对象

虚拟DOM 一般是一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对属性的命名不同。、

通过对虚拟DOM构成的抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能

image.png

2 虚拟DOM的优点

  1. 虚拟DOM可以将大量多次的DOM操作合并为一次操作,例如将1000次的DOM修改合并为1次,减少了操作DOM的频率

  2. 虚拟DOM可以通过DOM diff算法减少不必要的DOM操作,减少DOM的修改范围

  3. 虚拟DOM实际上是给我们找了一条最短,最近的路径,并不是说比DOM操作的更快。

  4. 虚拟DOM可以借助不依赖真实DOM结构的优势,实现更有效的跨平台兼容

3 虚拟DOM的缺点

  1. 虚拟DOM通过diff算法和批量处理策略,将所有的DOM操作整合,一次性去改变真实的DOM,但在首次渲染上,虚拟DOM会多了一层计算,可能会比html渲染的要慢
  2. 需要额外的渲染函数创建DOM,例如Vue中的h函数或React中的createElement,或者依赖打包工具通过 JSX或者vue-loader 来简化成 XML 写法。

DOM diff

1 什么是DOM diff算法

比较改变前后两个虚拟DOM树区别的算法一般称为DOM diff算法 一般有以下的比较方式

  • Tree diff

将新旧两棵树逐层对比,找出哪些节点需要更新

如果节点是组件就看 Component diff

如果节点是标签就看 Element diff

  • Component diff

如果节点是组件,就先看组件类型

类型不同直接替换(删除旧的)

类型相同则只更新属性

然后深入组件做 Tree diff(递归)

  • Element diff

如果节点是原生标签,则看标签名

标签名不同直接替换,相同则只更新属性

然后进入标签后代做 Tree diff(递归)

具体实现

以React为例

  • 两棵树如果完全比较时间复杂度是O(n^3)
  • ReactDiff算法的时间复杂度是O(n)。只在相同层级之间比较两棵树的节点,放弃了深度遍历,考虑到现实中前端页面跨层级移动DOM元素的行为相对较少,所以这样做是最优的。

DOM diff的操作一共分为四种:

img

  • 第一种是最简单的,节点类型变了,这个过程称之为REPLACE。直接将旧节点卸载(componentWillUnmount)并装载新节点(componentWillMount)就行了

  • 第二种也比较简单,节点类型一样,仅仅属性或属性值变了,这个过程称之为PROPS。此时不会触发节点的卸载(componentWillUnmount)和装载(componentWillMount)动作。而是执行节点更新(shouldComponentUpdatecomponentDidUpdate的一系列方法)

  • 第三种是文本变了,文本对也是一个Text Node,也比较简单,直接修改文字内容就行了,我们将这个过程称之为TEXT

  • 第四种是移动,增加,删除子节点,我们将这个过程称之为REORDER

diff算法的优点

减少对于DOM的操作,只需要对有变化的元素进行修改,提高效率

缺点

在同级操作中会出现BUG

image.png image.png 如上图所示,如果不用Key标识DOM元素,则插入第一个元素时,右侧对应的元素没有和左侧插入后的结果匹配

  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速
  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异
  • 尽量不要采用索引作为key
  • 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM,随后Vue进行新虚拟DOM和旧虚拟DOM的差异比较;比较规则如:\

  • 旧虚拟DOM中找到了与新虚拟DOM相同地key:如果虚拟DOM内容没有变化,则直接使用之前的真实DOM;如果虚拟DOM中内容发生改变,则生成新的真实DOM,随后替换掉页面之前的真实DOM;\
  • 旧虚拟DOM中未找到与新虚拟DOM相同地key,则创建新的真实DOM,随后渲染到页面。