VirtualDOM的实现原理
Virtual DOM
- Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象
- 真实 DOM 成员
真实 DOM 成员
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
}
console.log(s)Virtual DOM
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}为什么要使用 Virtual DOM
- 前端开发刀耕火种的时代
- MVVM 框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
- 虚拟 DOM 跟踪状态变化
- 参考 github 上 virtual-dom 的动机描述
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实 DOM
Virtual DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 跨平台
- 浏览器平台渲染 DOM
- 服务端渲染 SSR(Nuxt.js/Next.js)
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等

虚拟 DOM 库
- Snabbdom 注重简单性、模块化、强大特性和性能的虚拟 DOM 库
- Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
- 大约 200 SLOC(single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
- virtual-dom
Snabbdom 基础
基础使用
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建package.json
npm init -y
# 本地安装parcel
npm install parcel-bundler -D配置 scripts
{
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
},
}查看 node_modules\snabbdom\package.json 中的配置
exports是 node12 以后支持的,parcel和wabpcak4都是不支持这个字段,webpack5才开始支持这个字段
{
"exports": {
"./init": "./build/package/init.js",
"./h": "./build/package/h.js",
}
}演示 init、h、patch 的使用
import { init } from 'snabbdom/build/package/init.js'
import { h } from 'snabbdom/build/package/h.js'
const patch = init([])
// vnode第一个参数:标签+选择器
// vnode第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'Hello World')
let app = document.querySelector('#app')
// patch第一个参数:旧的VNode,可以是DOM元素
// patch第二个参数:新的VNode
// 返回新的VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode)演示用 h 函数创建 div,div 中可以创建子元素
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
let vnode = h('div#container', [h('h1', 'Hello Snabbdom'), h('p', '这是一个p')])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// vnode = h('div#container', [h('h1', 'Hello World'), h('p', 'Hello P')])
// patch(oldVnode, vnode)
// 清除div的内容
patch(oldVnode, h('!'))
}, 2000)模块
模块的作用
Snabbdom 的核心库并不能处理 DOM 元素的属性、样式、事件等
可以通过注册 Snabbdom 默认提供的模块来实现
Snabbdom 中的模块可以用来扩展 Snabbdom 的功能
Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
官方提供的模块
attributes
设置 vnode 对应 DOM 元素的属性
props
设置 DOM 对象的属性,通过
对象.属性方式设置,且不会处理布尔类型属性dataset
处理 html5 中的
data-这样的属性class
不是用来设置类样式的,是用来切换类样式的
style
设置行内样式,使用这个模块可以很容易设置动画
eventlisteners
注册和移除事件
模块使用步骤
- 导入需要的模块
init()中注册模块h()函数的第二个参数处使用模块
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 1.导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
// 2.注册模块
const patch = init([styleModule, eventListenersModule])
// 3.使用h()函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h(
'h1',
{
style: { backgroundColor: 'red' }
},
'Hello World'
),
h(
'p',
{
on: { click: eventHandler }
},
'Hello P'
)
])
function eventHandler() {
console.log('别点我')
}
let app = document.querySelector('#app')
patch(app, vnode)Snabbdom 核心
快捷键
F12定位变量定义位置,或按住CtrlAlt + ←返回刚刚的位置Alt + →跳转回定义位置按住
Ctrl点击弹出框头部地址栏跳转到对应代码定义
核心
- 使用
h()函数创建 JavaScript 对象(VNode)描述事实 DOM init()设置模块,创建patch()函数patch()比较新旧两个 VNode,第一个参数可以是 DOM 元素(通过tovnode.ts转换)- 把变化的内容更新到真实 DOM 树
git clone https://github.com/snabbdom/snabbdom.git
git checkout v2.1.0h 函数
作用:创建 VNode 对象
Vue 中的 h 函数
Vue 中的 h 函数实现了组件机制,snabdom 中没有
jsnew Vue({ router, store, render: h => h(App) }).$mount('#app')h 函数最早见于 hyperscript,使用 JavaScript 创建超文本
函数重载
- 参数个数或参数不同的函数(与参数有关与返回值无关)
- JavaScript 中没有重载的概念
- TypeScript 中有重载,不过重载的实现还是通过代码调整参数
// 函数重载-参数个数
function add (a: number, b: number) {
console.log(a + b)
}
function add (a: number, b: number, c: number) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
// 函数重载-参数类型
function add (a: number, b: number) {
console.log(a + b)
}
function add (a: number, b: string) {
console.log(a + b)
}
add(1, 2)
add(1, '2')// h 函数的重载()
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}
var children: any
var text: any
var i: number
// 处理参数,实现重载机制
if (c !== undefined) {
// 处理三个参数情况 sel、data、children/text
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
// 如果 c 是字符串或者数字
text = c
} else if (c && c.sel) {
// 如果 c 是 VNode
children = [c]
}
} else if (b !== undefined && b !== null) {
// 处理两个参数情况
// 如果 b 是数组
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
// 如果 b 是字符串或者数字
text = b
} else if (b && b.sel) {
// 如果 b 是 VNode
children = [b]
} else { data = b }
}
if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number,创建文本节点
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 如果是 svg,添加命名空间
addNS(data, children, sel)
}
// 返回 VNode
return vnode(sel, data, children, text, undefined)
};VNode
// 约束 VNode 对象具有的属性
export interface VNode {
// 选择器
sel: string | undefined
// 节点数据:属性/样式/事件等
data: VNodeData | undefined
// 子节点,和 text 互斥
children: Array<VNode | string> | undefined
// 记录 VNode 对应的真实 DOM
elm: Node | undefined
// 节点中的内容,核 children 互斥
text: string | undefined
// 唯一标识
key: Key | undefined
}
// 约束 VNodeData 的属性
export interface VNodeData {
props?: Props
attrs?: Attrs
class?: Classes
style?: VNodeStyle
dataset?: Dataset
on?: On
hero?: Hero
attachData?: AttachData
hook?: Hooks
key?: Key
ns?: string // for SVGs
fn?: () => VNode // for thunks
args?: any[] // for thunks
[key: string]: any // for any other 3rd party module
}
// 描述真实 DOM
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}init
调用 init 时返回了 patch,所以需要先了解 init

DOMAPI 里都是 DOM 操作,通过指定 DOMAPI 来决定如何转换虚拟 DOM,默认情况是把虚拟 DOM 转换成浏览器环境下的 DOM 对象


最后返回了 patch 函数(函数返回函数属于高阶函数)
- 使用高阶函数好处:init 的时候传入的参数可以进行缓存
modules和domApi,调用 patch 函数时就不需要传入那两个参数,只需要传入oldVnode和vnode

patch(打补丁)
patch(oldVnode, newVnode)- 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化
首次渲染需要真实 DOM

在 init 内部初始化了 cbs 对象,并且把所有模块钩子函数存储到 cbs 对应的属性中
const cbs: ModuleHooks = {
create: [], // [fn1, fn2...]
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}把真实 DOM 转换为 vnode 对象
api.tagName(elm).toLowerCase() + id + c标签名字 + id 选择器 + 类样式拼接起来作为 sel- data 是
{} - children 是
[] - text 是
undefined与 children 互斥

判断 vnode 的 key 和 sel 是否相同
