虚拟DOM与DOM diff

304 阅读4分钟

本文主要包含以下内容:

一、 虚拟DOM

  1. 虚拟DOM是什么
  2. 虚拟DOM的优点
  3. 虚拟DOM的缺点

二、 DOM diff

  1. DOM diff是什么
  2. DOM diff的大致逻辑
  3. DOM diff如何进行比较
  4. 解决DOM diff存在的问题

一、虚拟DOM

1. 虚拟DOM是什么?

1.1 DOM回顾

MDN对其解释如下:

文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。 事实上为了让网页实现各种各样的效果,浏览器给window添加了一个document对象,我们将网页抽象成一个document对象,通过这个document对象操作整个页面的所有的节点。上述模型Document Object Model 也就是DOM

我们每一次使用 js 操作 DOM,或者 DOM 每一次细微的改变,都会引发页面的重绘(paint)重排(layout),这也是我们经常所说的“DOM操作慢”的根本原因

1.2 虚拟DOM

虚拟DOM,也叫VDOM,一个能代表DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素以及其他属性。其本质是一个JS对象.它是仅存于内存中的DOM,因为还未展示到页面中,所以称为VDOM。

举个随处可见的例子:

var a = document.createElement("div");  //这就是VDOM

那么如何让VDOM变成真实的DOM呢?

-简单,只需将节点append到页面中:

var a = document.createElement("div");
document.body.append(a);   // 此时body内就会创建一个div标签,真实DOM
  • Vue、React中的虚拟DOM

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

2.虚拟DOM的优点

2.1 减少DOM操作

  • 减少DOM操作的次数,虚拟DOM可以将多次操作合并为一次操作,例如在div中添加1000个甚至更多的节点,常规的方法是一个接一个的操作的。

  • 减少DOM操作的范围,虚拟DOM借助DOM diff(后文会说)可以把多余的操作去掉,例如添加1000个节点,实际上只有10个节点是新增的,DOM diff会将其简化操作,而不是重绘。

2.2 能够跨平台渲染

虚拟DOM不仅能够变成DOM,还可以变成小程序、ios应用、安卓应用,因为其本质是一个JS对象。

3. 虚拟DOM的缺点

需要通过额外的创建函数,如React需要creatElement,Vue 需要h函数等,但是可以通过JSX语法(React)Vue-loader(Vue)来解决。如下:

**Vue Template**
通过`Vue-loader`转化为`h`形式
<div class="red" @click="fn">
 <span>span1</span>
 <span>span2</span>
 </div>
 -----------------------------------------------
**React JSX**
通过`bable`转化`creatElemet`形式
 <div className="red" onClick={fn}>
 <span>span1</span>
 <span>span2</span>
 </div>

二、DOM diff

1. 什么是DOM diff

DOM diff两个虚拟DOM树对比的算法,diff 算法仅在两个树的同级的虚拟节点之间做比较,递归地进行比较,最终实现整个 DOM 树的更新。

DOM diff本质上就是一个函数,我们称之为patch函数。有两个参数,分别是旧的虚拟DOM和新的虚拟DOM

patches=patch(oldVnode,newVnode)

patches就是要运行的DOM操作,类似于:

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

2. DOM diff的大致逻辑

  • tree diff(层级比较)

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

    2.如果节点是组件就看Component diff

    3.如果节点是标签就看Element diff

  • Component diff

    1.如果节点是组件,就先看组件类型,类型不同直接替换(删除旧的),类型相同则更新属性。

    2.然后深入组件做 tree diff

  • Element diff

    1.如果节点是原生标签,则看标签名,标签名不同直接替换,相同则更新属性。

    2.然后进入标签后代做tree diff

3.DOM diff如何进行比较

如下代码:

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

以上代码树的结构图可用下图表示: image.png

我们默认所有节点都是可变化的,假定div中的class变成了red,于是patch函数就会开始遍历并进行比较。

image.png

下面我们再变化,将第一个span删除。

image.png 计算机会认为是第一个span更新了,第二个span删除了!这就是DOM diff所存在的问题。

4. 解决DOM diff存在的问题

那么有什么办法可以解决DOM diff存在的问题呢?答案就是使用key(react跟vue都需要使用单独的key,这个key在同属于一个父节点的各兄弟节点上需要不同。)

如下:

//React JSX语法
<div>
{[{id:1,value:1},{id:2,value:2},{id:3,value:3}].map((v)=>{
   return <span key={v.id}/>
})}
</div>