一、Virtual DOM介绍
1. 什么是Virtual DOM
- Virtual DOM(虚拟DOM),是由普通的JS对象描述DOM对象,因为不是真实的DOM对象,所以叫做Virtual DOM
- 真实的DOM成员 非常非常多,所以创建一个DOM对象的成本非常高
- 可以通过Virtual DOM来描述真实DOM,示例:
{
sel: 'div',
data: {},
text: 'Hello Virtual DOM',
elm: undefined,
key: undefined
}
- Virtual DOM对象是非常小的,我们创建一个Virtual DOM的成本比创建一个真实DOM的成本小很多
2. 为什么使用Virtual DOM
- 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂 DOM操作复杂提升
- 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM 框架解决了视图和状态的同步问题
- 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了 Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM, Virtual DOM内部 将弄清楚如何有效(diff)的更新DOM
- 参考github上virtual-dom的描述 虚拟DOM可以维护程序的状态,跟踪上一次的状态 通过比较前后两次状态的差异更新真实DOM
3. 虚拟DOM的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native )、小程序(mpvue/uni-app)等
4. Virtual DOM库
-
Snabbdom
- Vue2.x内部使用的Virtual DOM 就是改造的Snabbdom
- 大约200SLOC(Single Line Of Code)
- 通过模块可扩展
- 源码使用TypeScript开发
- 最快的Virtual DOM之一
-
Virtual DOM
二、Snabbdom基本使用
1. 创建项目
-
- 打包工具为了方便使用Parcel
-
- 创建项目,并安装Parcel
mkdir snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建package.json
yarn init -y
# 本地安装Parcel
yarn add parcel-bundler
# 安装snabbdom@2.1.0
yarn add snabbdom@2.1.0
- 3. 配置package.json的script
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
}
-
- 创建目录结构
|-index.html
|-package.json
|-src
|-01.basicusage.js
2. 导入Snabbdom
-
Snabbdom的官网demo中导入使用的是CommonJS模块化语法,我们使用的是更流行的ES6模块化的语法import
-
关于模块化的语法可以参考阮一峰老师的《Module的语法》
-
ES6模块化与CommonJS模块的差异
-
import { init } from 'snabbdom/build/package/init'
-
import { h } from 'snabbdom/build/package/h'
-
Snabbdom的核心仅提供最基本的功能,只导出了三个函数init()、h()、thunk()
-
init()是一个高阶函数,返回patch()
-
h()返回虚拟节点VNode,这个函数我们在使用Vue.js的时候用过
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
-
thunk()是一种优化策略,可以在处理不可变数据时使用
-
注意:导入时候不能使用import snabbdom from 'snabbdom' 原因:node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用export default导出默认导出
3. Snabbdom的基本用法
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')
第一个参数:旧的VNode, 可以是DOM 元素 第二个参数:新的VNode 返回新的VNode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode)
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 World'),
h('p', '这是一个p')
])
let app = document.querySelector('#app')
let oldVonde = patch(app, vnode)
setTimeout(() => {
// vnode = h('div#containwe', [
// h('h1', 'Hello Snabbdom'),
// h('p', 'Hello p')
// ])
// patch(oldVonde, vnode)
// 清楚div中的内容
patch(oldVonde, h('!'))
}, 2000)
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'
- 注册模块
const patch = init([
styleModule,
eventListenersModule
])
3.使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor:'red'}}, 'Hello World'),
h('p', { on: { click: enentHandle }}, 'Hell P')
])
function enentHandle() {
console.log('别点我, 疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
4. Snabbdom中的模块
Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
-
官方提供了6个模块
-
- attributes 设置了DOM元素的属性,使用setAttribute() 处理布尔类型的属性
-
- props 和attributes模块相似,设置DOM元素的属性element[attr] = value 不处理布尔类型的属性
-
- class 切换类样式 注意:给元素设置类样式是通过set选择器
-
- dataset 设置data-*的自定义属性
- 5.eventlisteners 注册和移除事件
-
- style设置行内样式,支持动画 delayed/remove/destroy
-
-
-
模块使用
-
- 导入需要的模块
-
- init()中注册模块
-
- 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
-
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: enentHandle }}, 'Hell P')
])
function enentHandle() {
console.log('别点我, 疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
5. Snabbdom源码解析
-
-
如何学习源码
- 先宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
-
-
-
Snabbdom的核心
- 使用h()函数创建JavaScript对象(VNode)描述真实DOM
- init设置模块,创建patch()
- patch()比较新旧两个VNode
- 把变化的内容更新到真实DOM树上
-
-
-
Snabbdom源码
- 源码地址:
- github.com/snabbdom/sn…
-
h函数
-
h函数介绍
- 在使用Vue的时候见过h函数 new Vue({ router, store, render: h => h(App) }).$mount('#app')
- h函数最早见于hyperscript,使用JavaScript创建超文本
- Snabbdom中的h函数不是用来创建超文本,而是创建VNode
-
函数重载
-
概念
- 参数个数或者类型不同的函数
- JavaScript中没有重载的概念
- TypeScript中有从在,不过重载的实现还是通过代码调整参数
-
重载的示意
-
function add (a, b) {
console.log(a + b)
}
function add (a, b, c) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
patch函数
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧VNode是否相同节点(节点的key和sel相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的VNode是否有text, 如果有并且和oldVnode的text不同,直接更新文本内容
- 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
- diff 过程只进行同层级比较