Virtual DOM 的实现原理
- 了解什么是虚拟DOM,以及虚拟DOM的作用
- Snabbdom的基本使用
- Snabbdom的源码解析
一、什么是虚拟DOM ----Virtual DOM
-
虚拟DOM是由普通的JS对象来描述DOM对象
二、 为什么要使用Virtual DOM
- 前端开发初期,MVVM框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
- 虚拟DOM跟踪状态变化
- 参考GitHub上Virtual-dom的动机描述
- 虚拟DOM可以维护程序上的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实DOM
虚拟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 的基本使用
- 1、基本步骤:
- 初始化项目目录并安装Parcel
//创建项目目录
md snabbdom-demo
//进入项目目录
cd snabbdom-demo
//创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler -D
- 配置package.json中的scripts
"scripts:"{
"dev":"parcel index.html --open",
"build":"parcel build index.html"
}
- 创建目录结构
- 根目录创建index.html,引入src目录中的文件
- 在src中创建js文件来导入使用的snabbdom进行编码
- 2、导入Snabbdom
Snabbdom 文档:
英文:https://github.com/snabbdom/snabbdom
中文:https://github.com/coconilu/Blog/issues/152
当前版本:v2.1.0
//导入snabbdom
npm install snabbdom@2.1.0
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
- 3、案例1
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// patch函数中的第一个参数:旧的VNode,也可以是DOM元素
// 第二个参数:新的VNode
// 返回新的VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx','hello Snabbdom')
patch(oldVnode,vnode)
- 4、案例2
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', '这是一个段落'),
])
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中的内容 h('!')-->生成空的节点
patch(oldVnode, h('!'))
}, 2000)
五、 Snabbdom 模块的使用
- 模块的作用
- Snabbdom 的核心库并不能处理DOM元素的属性、样式、事件等,可以通过注册Snabbdom默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展Snabbdom的功能
- Snabbdom中的模块的实现是通过注册全局的钩子来实现的
- 官方提供的模块
- attributes 设置元素属性 会处理布尔类型的属性
- props 设置元素属性 不会处理布尔类型的属性
- 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: { background: 'red' } }, 'hello World'),
h('p', { on: { click: eventHandler } }, 'hello p'),
])
function eventHandler() {
console.log('点击了')
}
let app = document.querySelector('#app')
patch(app, vnode)
六、 Snabbdom 源码解析
- 如何学习源码
- 宏观了解
- 待着目标看源码
- 看源码的过程要围绕核心目标
- 调试
- 参考资料
- Snabbdom的核心
- init()设置模块,创建patch()函数
- 使用h()函数创建javascript对象(VNode)描述真实DOM、
- patch()比较新旧两个Vnode
- 把变化的内容更新到真实的DOM树
- 源码地址
- 英文:https://github.com/snabbdom/snabbdom
- 中文:https://github.com/coconilu/Blog/issues/152
-当前版本:V2.1.0
- 克隆代码
- git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
npm install
npm run build
查看
七、 h() 函数
- h函数介绍
- 作用:创建vNode 对象
- vue中的h函数
- h 函数最早见于hyperscript,使用JavaScript创建超文本
//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);
}
add(1,2)
add(1,'2')
// 函数的重载
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
// 如果c是字符串或者数字
} else if (is.primitive(c)) {
text = c
// 如果c 是VNode
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
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)
};
八、 快捷键
快速定位
Alt + ←向左方向键
ctrl+鼠标左键
九、 VNode
十、 Patch 整体过程分析
- patch(oldVnode,newVnode)
- 把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧VNode是否相同节点(节点的key和sel相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode 的text不同,直接更新文本内容
- 如果有新的VNode有children,判断子节点是否有变化
十一、patchVnode
十二、 Diff 算法
-
虚拟DOM中的Differences算法
-
查找两棵树每一个节点的差异
-
-
Snabbdom 根据DOM的特点对传统的diff算法做了优化
- DOM操作时候很少会跨级别操作节点
-
只比较同级别的节点
-
对比子节点的具体过程,在对开始和结束节点比较的时候,共有四种情况
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
-
开始和结束节点
- 如果新旧开始节点是sameVnode(key和sel相同)
- 调用patchVnode()对比和更新节点
-
把旧开始和新开始索引往后移动 oldStartIdx ++ / newStartIdx ++
- 如果新旧开始节点是sameVnode(key和sel相同)
-
旧开始节点 / 新结束节点
- 调用patchVnode()对比和更新节点
-
把oldStartVnode对应的DOM元素,移动到右边,更新索引
-
旧结束节点 / 新开始节点
- 调用patchVnode()对比和更新节点
-
把oldStartVnode对应的DOM元素,移动到左边,更新索引
-
非上述四种情况
-
循环结束
- 当老节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束
- 当新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束
-
oldStartIdx > oldEndIdx
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
-
说明新节点有剩余,把剩余节点批量插入到右边
-
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
-
newStartIdx >newEndIdx
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
-
说明老节点有剩余,把剩余节点批量删除
-
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx)