这次我们的学习目标有三:
1.了解什么是虚拟DOM,以及虚拟DOM的作用
2.Snabbdom的基本使用
3.Snabbdom的源码解析
什么是Virtual DOM
- Virtual DOM就是虚拟DOM,是由普通的JS对象来描述DOM对象
-
使用Virtual DOM来描述真实的DOM
为什么要使用Virtual DOM?
- DOM的操作本身是性能会出现问题,操作比较复杂的
- MVVM框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
(无法得知当前页面变化之前的状态) - 虚拟DOM能够跟踪状态变化
- 参考github上virtual-dom的动机描述
- 虚拟DOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实DOM
实际案例
传统DOM操作方式:
https://codesandbox.io/s/jquery-demo-yr65q
虚拟DOM操作方式:
https://obk5t.csb.app/
通过实例我们就可以轻易区分出两者的不同之处
Virtual DOM的作用
- 维护视图与状态的关系
- 复杂视图情况下,提升渲染性能
- 跨平台
- 浏览器平台渲染DOM
- 服务端渲染SSR(Nuxt.js/Next.js)
- 原生应用(Weex,React Native)
- 小程序(mpvue/uni-app)等等
虚拟DOM库
- Snabbdom
- Vue.js 2.x内部使用的虚拟DOM,就是改造的Snabbdom
- 大约200 SLOC
- 通过模块可拓展
- 源码使用TS开发
- 最快的Virtual DOM之一
- virtual-dom
Snabbdom基本使用方式
- 安装parcel
- 配置scripts
-
目录结构
Snabbdom 文档
https://github.com/snabbdom/snabbdom
当前版本:v2.1.0
官方文档中文翻译:
https://github.com/coconilu/Blog/issues/152
根据文档所述:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
console.log(init)
console.log(h)
我们引入两个核心功能,这里要注意一下,因为webpack版本问题,我们按照官方文档那样引入的话会出现加载错误的问题,所以我们应该按照如上代码依次查询路径导入
打印一下init和h方法,可以看到我们主要使用的方法的具体内容
基本使用:
- 主要用到了init函数和h函数
- h函数有两个参数,第一个参数是新定义的标签(包含class和id),第二个参数是新的内容(传入字符串)
- 要获取挂载的元素,通过init函数得到patch函数,第一次声明patch,init内部得是空数组
- patch有两个参数,第一个是挂载的元素dom,第二个是通过h函数创建的vnode
详细来看就是这样的:
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)
包含子节点
跟基本使用大体上是一样的,但是有一点不同,h函数的第二个参数,如果是数组的话表示子节点列表
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 创建包含子节点的VNode
// h的参数二为子节点列表,内部就应该传入vNode
let vNode = h('div#container', [
h('h1', '标题1'),
h('p', '内容1')
])
// 获取挂载元素
const dom = document.querySelector('#app')
// 渲染vNode
patch(dom, vNode)
函数参数为!时表示清空
模块使用
- 模块的作用
- 官方提供的模块
- 模块的使用步骤
模板的作用
- Snabbdom的核心库并不能处理DOM元素的属性/样式/事件等等,可以通过注册Snabbdom默认提供的模块来实现
- Snabbdom中的模块可以用来拓展Snabbdom的功能
- Snabbdom中的模块的实现是可以通过注册全局的钩子函数来实现的
官方提供的模块
- 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'
console.log(styleModule);
console.log(eventListenersModule)
// 2.注册模块(为patch函数添加模块对应的能力)
const patch = init([
styleModule,
eventListenersModule
])
// 3.使用模块
let vNode = h('div#box', {
style: {
backgroundColor: 'green',
height: '200px',
width: '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函数创建JS对象(VNode)描述真实DOM
- patch()比较新旧两个VNode
- 把变化的内容更新到真实的DOM树
源码
- 地址:
https://github.com/snabbdom/snabbdom
- 克隆代码
git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
h函数
- 作用:创建VNode对象
-
Vue的h函数
函数重载
- 参数个数或者参数类型不同的函数
- JS没有重载的概念
- TypeScript中有重载,不过重载的实现还是通过代码调整参数
patch整理过程分析
- patch(oldVnode,newVnode)
- 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧VNode是否相同节点(节点的key与sel相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
- 如果新的VNode有children,判断子节点是否有变化
init
patch
createElm
在patch函数中使用到的createElm
patchVnode
updateChildren
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key as string]
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
注:后续会专门挨着挨着解析源码的作用