虚拟DOM和DOM diff

362 阅读4分钟

DOM节点

一个标签,一段文本,甚至一个注释都是一个DOM节点,如图所示:

DOM节点种类有很多,共12种节点,常用节点就以下四种:

  1. document — DOM 的“入口点”。
  2. 元素节点 — HTML 标签,树构建块。
  3. 文本节点 — 包含文本。
  4. 注释 — 有时我们可以将一些信息放入其中,它不会显示,但 JS 可以从 DOM 中读取它。
<!DOCTYPE HTML>
<html>
<head>
  <title>About elk</title>
</head>
<body>
  The truth about elk.
</body>
</html>

在DOM文档对象中会生成以下节点:

HTML
	HEAD
		#text ↵␣␣␣␣
		TITLE
			#text About elk
		#text ↵␣␣
	#text ↵␣␣
	BODY
		#text The truth about elk.

标签被称为元素节点(或者仅仅是元素),并形成了树状结构:<html> 在根节点,<head><body> 是其子项,等。

元素内的文本形成文本节点,被标记为 #text。一个文本节点只包含一个字符串。它没有子项,并且总是树的叶子。

请注意文本节点中的特殊字符:

  • 换行符:(在 JavaScript 中为 \n
  • 空格:

只有两个顶级排除项:

  • 由于历史原因,<head> 之前的空格和换行符均被忽略。
  • 如果我们在 </body> 之后放置一些东西,那么它会被自动移动到 body 内,并处于 body 中的最下方,因为 HTML 规范要求所有内容必须位于 内。所以 之后不能有空格。 空格和换行符都是完全有效的字符,就像字母和数字。它们形成文本节点并成为 DOM 的一部分。

原生JS操作DOM

其实JS操作DOM并不慢,只是浏览器计算布局慢。说JS操作DOM慢其实是对比原生JS的API,像JS执行数组、对象等api。

浏览器呈现页面的步骤大概分为以下几点:

  • 利用网络去请求内容,如果命中缓存则直接读取缓存
  • 获取内容后,HTML解释器解析HTML,生成DOM tree
  • 解析到style标签时,CSS解析器解析css样式
  • 解析到JavaScript时,停止解析,JavaScript引擎开始执行js脚本
  • DOM tree与解析后的CSS共同构建成一个render tree
  • 计算布局,再绘制

浏览器渲染引擎在计算布局时是最耗时的,

/*HTML*/
<body>
    <button id="btn"></button>
    <span id="content1"></span>
    <span id="content2"></span>
    <div id="addChild"></div>
</body>
const content1 = document.getElementById('content1')
const content2 = document.getElementById('content2')
const addChild = document.getElementById('addChild')
const btn = document.getElementById('btn')

const n = 100000
const time = new Date()
btn.innerText = `添加${n}个div`
btn.onclick = () => {
    for (let i = 0; i < n; i++) {
        const div = document.createElement('div')
        div.innerText = i
        addChild.appendChild(div)
    }
    setTimeout(() => {
        const renderTime = new Date() - time
        content2.innerText = `浏览器渲染时间:${renderTime}ms;`
    })
    const jsTime = new Date() - time
    content1.innerText = `js执行时间:${jsTime}ms`
}

执行上面代码,截图如下;

虽然现实中不可能操作100000个元素,这里只是为了显示JS操作DOM和浏览器渲染时间的对比。Vue、React封装了DOM操作,但归根结底还是js操作DOM。不同的是它们都运用了虚拟DOM,增添了DOM diff的操作。

虚拟DOM

减少DOM操作

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

跨平台

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

Vue/React建立虚拟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'])

从上就可以总结出:

  1. 虚拟 DOM 是什么

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

  2. 虚拟 DOM 有什么优点

    能减少不必要的 DOM 操作,能跨平台渲染

  3. 虚拟 DOM 有什么缺点

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

DOM diff

什么是 DOM diff ?

  • DOM diff就是一个函数,
  • 我们称之为 patch, patches = patch(oldVNode, newVNode)
  • patches 就是要运行的 DOM 操作

具体过程:

  • Tree diff

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

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

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

缺点:

  • 如在页面加载[1,2,3]
  • 同时做删除2的操作,剩下[1,3]
  • 而浏览器会将2变成3,再删除3

浏览器重绘如下: (绿色代表更改,红色代表删除)

这也是vue的v-for为什么要加:key的原因,DOM diff时li为2的:key不存在了,就直接删除掉,再加载:key=3的li

综上:

原生:修改-删除

Vue:删除-添加(代码变化更多)