Virtual DOM
什么是 Virtual DOM
- Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象
- 真实 DOM 成员
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
}
console.log(s)
{
sel: "div",
data: {},
children: undefined,
text: "hello Virtual DOOM"
elm: undefined,
key: undefined
}
为什么要使用 Virtual DOM
- 前端开发刀耕火种的时代
- MVVM 框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
- 虚拟 DOM 跟踪状态变化
- 参考 github 上 virtual-dom 的动机描述
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实 DOM
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 跨平台
- 浏览器平台渲染DOM
- 服务端渲染 SSR(Nuxt.js/Next.js)
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
虚拟 DOM 库
- Snabbdom
- Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
- 大约 200 SLOC (single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
- virtual-dom
Snabbdom 基本使用
安装 parcel
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建package.json
npm init -y
# 本地安装parcel
npm install parcel-bundler -D
配置 script
"scripts": {
"dev" : "parcel index.html --open",
"build" : "parcel build index.html"
}
目录结构
- index.html
- package.json
- src
导入 Snabbdom
Snabbdom 文档
- 看文档的意义
- 学习任何一个库都要先看文档
- 通过文档了解库的作用
- 看文档中提供的示例,自己快速实现一个 demo
- 通过文档查看 API 的使用
- Snabbdom 文档
- https://github.com/snabbdom/snabbdom
- 当前版本 v2.1.0
导入 Snabbdom
- 安装 Snabbdom
- npm install snabbdom@2.1.0
- 导入 Snabbdom
- Snabbdom 的两个核心函数 init 和 h()
- init() 是一个高阶函数,返回 patch()
- h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js的时候见过
- 文档中导入的方式
import { init } from 'snabbdom/init'
import { h } from 'snabbdom/h'
const patch = init([])
- 实际导入的方式
- parcel/webpack 4 不支持package.json 中的 exports 字段
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
Snabbdom 基本使用
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1 通过h函数创建vNode
let vNode = h('div#box.container', '新内容')
// 获取挂载元素
const dom = document.querySelector('#app')
// 2 通过init函数得到patch函数
const patch = init([])
// 3 通过patch,将vNode渲染到DOM
let oldVNode = patch(dom, vNode)
// 4 创建新的 VNode,更新给oldVNode
vNode = h('p#text.abc','这是p标签的内容')
patch(oldVNode, vNode)
包含子节点
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 创建包含子节点的VNode
// -参数2的数组中为子节点列表,内部就应该传入VNode
let vNode = h('div#container', [
h('h1', '标题文本'),
h('p', '内容文本')
])
// 获取挂载元素
const dom = document.querySelector('#app')
// 渲染vNode
const oldVNode = patch(dom, vNode)
// 生成注释节点,清空内容
// patch(oldVNode, h('!'))
Snabbdom 中的模块
模块的作用
- Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展 Snabbdom的功能
- Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
官方提供的模块 Module
- attributes
- props
- dataset
- class
- style
- eventlisteners
模块的使用步骤
- 导入需要的模块
- init() 中注册模块
- h() 函数的第二个参数处使用模块
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1.导入模块
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
// 2.注册模块(为patch函数添加模块对应的能力)
const patch = init([
styleModule,
eventListenersModule
])
// 3.使用模块
let vNode = h('div#box', {
style: {
backgroundColor: 'green',
height: '200px',
weight: '200px'
}
},[
h('h1#title', {
style: {
color: '#fff'
},
on: {
click () {
console.log('点击了h1标签')
}
}
}, '这是标题内容'),
h('p','这是内容文本')
])
const dom = document.getElementById('app')
patch(dom, vNode)
Snabbdom 源码解析
如何学习源码
- 宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
Snabbdom 的核心
- init() 设置模块,创建 patch() 函数
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM树
Snabbdom 源码
- 源码地址
- https://github.com/snabbdom/snabbdom
- 当前版本:v2.1.0
- 克隆代码
- git clone -b v2.1.0 --depth=1
https://github.com/snabbdom/snabbdom.git
Snabbdom 源码解析 - h 函数
h 函数介绍
- 作用:创建 VNode 对象
- Vue 中的 h 函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
函数重载
- 参数个数或参数类型不同的函数
- 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+c)
}
add(1, 2)
add(1, '2')
Snabbdom 源码解析 - vnode
Snabbdom 源码解析 - patch 整体过程分析
- patch(oldVnode, newVnode)
- 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化
Snabbdom 源码解析 - init
Snabbdom 源码解析 - patch
Snabbdom 源码解析 - createElm
Snabbdom 源码解析 - patchVnode
Snabbdom 源码解析 - updateChildren 整体分析
Diff 算法
- 虚拟 DOM 中的 Diff 算法
- Snabbdom 根据 DOM 的特点对传统的diff算法做了优化
- DOM 操作时候很少会跨级别操作节点
- 只比较同级别的节点
执行过程
- 在对开始和结束节点比较的时候,总共有四种情
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点
新旧开始节点
- 如果新旧开始节点是 sameVnode (key 和 sel 相同)
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++
旧开始节点 / 新结束节点
- 调用 patchVnode() 对比和更新节点
- 把 oldStartVnode 对应的 DOM 元素,移动到右边,更新
旧结束节点 / 新开始节点
- 调用 patchVnode() 对比和更新节点
- 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新
非上述四种情况
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
- 如果没有找到,说明 newStartNode 是新节点
- 创建新节点对应的 DOM 元素,插入到 DOM 树中
- 如果找到了
- 判断新节点和找到的老节点的 sel 选择器是否相同
- 如果不相同,说明节点被修改了
- 重新创建对应的 DOM 元素,插入到 DOM 树中
- 如果相同,把 elmToMove 对应的 DOM
循环结束
- 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
- 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
oldStartIdx > oldEndId
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
newStartIdx > newEndI
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
Snabbdom 源码解析 - updateChildren
Snabbdom 源码解析 - 调试 updateChildren
Snabbdom 源码解析 - 调试带 key 的情况
节点对比过程
Snabbdom 源码解析 - Key 的意义