virtual DOM
一般将 virtual DOM
简写为 vdom
。vdom
的目的是以最优解去更新 DOM
,那么就要保证更新节点的查询次数最少(vnode
更新节点不需要查询),更新的影响范围最小(依靠 vnode.elm
精确定位节点)。
vdom 定义
含义:用 JS 对象来映射模拟
DOM
结构h('div', { id: 'test'}, 'hello') // 生成 vnode { children: undefined, // 数组,表示当前节点的 Element 类型子节点的子 vnode data: { // 当前节点的属性 id: 'data' }, elm: div, // 映射真实 DOM 树节点,即此处是 vdom 精确更新修改后节点的关键之一 key: undefined, sel: 'div', // h() 第一参数,用于创建真实节点 text: 'hello' // 当前节点的文本节点,与 children 属性互斥 }
1
2
3
4
5
6
7
8
9
10
11
12原理:在 JS 层通过
diff
算法来对比DOM
的结构变化浏览器中最耗时的操作即是
DOM
操作,直接操作DOM
,将带来巨大的性能开销,所以在没有使用vdom
时,需要我们尽可能减少DOM
操作。JS 的执行速度远远快于浏览器
DOM
渲染速度。所以在 JS 层来处理DOM
结构变化将大大降低性能开销。结果将是以最优解(最小的改动范围,最少的改动次数)去改变DOM
结构。借助
diff
算法对比新旧vnode
的差异。vnode.elm
与真实DOM
树节点形成映射,进行精准更新,避免了DOM
节点查询。
前端中只有 JS 是图灵完备语言 —— 能执行算法的语言
目的:提高重绘性能。即是
vdom
存在的意义。
vdom API
核心:
h
函数:生成vnode
。patch
函数:对比新旧vnode
,并生成或更新DOM
节点。API 示例如下:
// const snabbdom = require('sanbbdom') import snabbdom from 'snabbdom' import h from 'snabbdom/h' import snabbdom_class from 'snabbdom/modules/class' import snabbdom_props from 'snabbdom/modules/props' import snabbdom_style from 'snabbdom/modules/style' import snabbdom_eventlistener from 'snabbdom/modules/eventlistener' // 初始化 patch const patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) // 生成 vnode h(`${/* 标签名 */}`, {/* 属性 */}, [/* 子元素 */]) h(`${/* 标签名 */}`, {/* 属性 */}, `${/* 文本节点 */}`)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 容器元素 const container = document.querySelector('#container') // 初次渲染:vnode 初次渲染至 container 容器中 patch(container, vnode) // 更新节点:利用 js 对比新旧 vnode 来得到操作 DOM 的最优解 patch(vnode, newVnode)
1
2
3
4
5
6
7
8示例:生成
table
表格const data = [ { name: 'name', age: 'age', address: 'address' }, { name: 'John Wick', age: 20, address: 'Shanghai' } ] let vnode = {} // 缓存容器 function render () { // 生成 vnode const newVnode = h('table', {}, data.map(function (item) { const tds = [] Object.keys(item).forEach(key => { tds.push(h('td', {}, item[key])) }) // 返回一个以 tr 元素的 vnode 组成的数组 return h('tr', {}, tds) })) if (vnode) { patch(vnode, newVnode) } else { patch(container, newVnode) } // 缓存 vnode vnode = newVnode } render(data)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
得到表格如下:
name | age | address |
---|---|---|
John Wick | 20 | Shanghai |
DOM
树如下:
<div id="container">
<table>
<tr>
<td>name</td>
<td>age</td>
<td>address</td>
</tr>
<tr>
<td>John Wick</td>
<td>20</td>
<td>Shanghai</td>
</tr>
</table>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
diff 算法 —— vdom 核心
定义
linux
中的diff
命令。diff a.js a1.js # 将打印 a.js 与 a1.js 的差异
1
2diff
算法并非vdom
的原创算法。
vdom
使用diff
算法的原因- 依靠
diff
算法,可以找到新旧vnode
中的差异点,即是必须更新的节点(依据vnode.elm
得到映射的真实DOM
树节点进更新),而其他节点则保持不变。
- 依靠
diff
算法核心实现流程初次渲染,将
vnode
初次转换成DOM
节点patch(container, vnode)
1// 仅用于体现转换逻辑,并非真实体现 function createElement () { const tag = vnode.sel const attrs = vnode.data || {} const children = vnode.children || [] if (!tag) return null // 创建元素 const ele = document.createElement(tag) // 设置节点属性 Object.keys(attrs).forEach(item => { ele.setAttribute(item, attrs[item]) }) // 插入子元素 children.forEach(childVnode => { // 递归创建子节点的后代节点(若有的话) ele.appendChild(createElement(childVnode)) } // 返回真正 DOM 节点 return ele }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24以上逻辑主要展现
vnode
的初次渲染。于patch
方法中,container
为渲染容器。渲染更新
patch(vnode, newVnode)
1function updateChildren (vnode, newVnode) { const children = vnode.children || [] const newChildren = newVnode.children || [] children.forEach((childVnode, index) => { const newChildVnode = newChildren[index] if (childVnode.sel === newChildVnode.sel) { // 递归子节点的后代节点进行对比 updateChildren(childVnode newChildVnode) } else { replaceNode(childVnode, newChildVnode) } }) } function replaceNode(vnode, newVnode) { const elm = vnode.elm const newElm = createElement(newVnode) // DOM 替换 ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21以上逻辑主要展现
vnode
更新。在得到newVnode
后传入patch
函数,patch
函数将根据diff
算法得到vnode
与newVnode
之间的差异,之后执行精确的DOM
更新。