Virtual DOM 的实现原理
1)Virtual DOM
- Virtual DOM (虚拟 DOM ),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
- 真实 DOM
let element = document.querySelector('#app')
let s = ''
for (const key in element) {
s += key + ','
}
console.log(s)
- 可以使用 Virtual DOM 来描述真实 DOM , 示例
{
sel: 'div',
data: {},
children: undefined,
text: 'heloo virtual dom',
elm: undefined,
key: undefined
}
-
为什么使用 virtual DOM
- 手动操作 DOM 笔记麻烦,还需要考虑浏览器兼容性问题
- 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
- 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 virtual DOM 出现了
- virtual DOM 的好处是当状态改变时不需要立即更新 DOM, 只需要创建一个虚拟树来描述 DOM, virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
- 参考 github 上 virtual-dom 的描述
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态的差异更新真实 DOM
-
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 处理渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等
- virtual DOM 库
- Snabbdom
- Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
- 大约 200 SLOC
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
- virtual-dom
- Snabbdom
2)Snabbdom 基本使用
-
创建项目
- 打包工具为了方便使用 parcel
- 创建项目,并安装 parcel
md snabbdom-dom cd snabbdom-dom yarn init -y yarn add parcel-bundler --dev
- 配置 package.json 的 scripts
"scripts": { "dev": "parcel index.html --open", "build": "parcel build index.html" },
- 创建目录结构
| index.html | package.json |_src | 01-basicusage.js
-
导入项目
- 安装
yarn add snabbdom
- "snabbdom": "^2.1.0" (与低版本使用 0.7.4 有区别)
import { h } from 'snabbdom/build/package/h' import { thunk } from 'snabbdom/build/package/thunk' import { init } from 'snabbdom/build/package/init'
注意:
导入的时候不能使用
import snabbdom from 'snabbdom'
, 因为 `node_modules/snabbdom/src/package/*.ts 末尾导出使用的语法是 **export 导出 API **,**没有使用 export default 导出默认输出 **
-
snabbdom 的核心仅提供最基本的功能
- snabbdom.init 核心仅暴露出一个函数
snabbdom.init
。init
接收一个模块列表,并返回一个使用指定模块集的patch
函数。 - snabbdom/h 建议使用
snabbdom/h
创建虚拟节点(vnodes)。h
函数接收一个字符串形式的标签/选择器、一个可选的数据对象、一个可选的字符串或数组作为子代。 - thunk 是一个优化策略,可用于处理不可变数据
- snabbdom.init 核心仅暴露出一个函数
-
基本案例
-
patch
init
返回的patch
函数有两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示更新后的新视图的vnode。如果传递带有父节点的DOM元素,
newVnode
将被转换为DOM节点,传递的元素将被创建的DOM节点替换。如果传递旧的vnode, Snabbdom将有效地修改它以匹配新vnode中的描述。传递的任何旧vnode都必须是上一个
patch
调用的结果vnode。这是必要的,因为Snabbdom将信息存储在vnode中。这使得实现更简单、更高性能的体系结构成为可能。这也避免了创建新的旧vnode树。
// 01-basicusage.js import { h } from 'snabbdom/build/package/h' import { init } from 'snabbdom/build/package/init' // import { thunk } from 'snabbdom/build/package/thunk' // import { classModule } from 'snabbdom/build/package/modules/class' // console.log(h, thunk, init, classModule) // 1. 实现一个 hello world // init 参数:数组,模块 返回值:patch函数,作用对比两个 VNode 的差异更新到真实 DOM let patch = init([]) // h 参数(标签+选择器,如果是字符串的话就是标签中的内容) let vnode = h('div#container.cls', 'hello world') let app = document.querySelector('#app') // patch函数有两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示更新后的新视图的vnode。 // 第一个参数:可以是 DOM 元素,内部会把 DOM 元素转换成 VNode // 第二个参数:VNode // 返回值:VNode let oldVnode = patch(app, vnode) // 更新数据 let newVnode = h('div', 'hello snabbdom') patch(oldVnode, newVnode)
// 02-basicusage.js import { h } from 'snabbdom/build/package/h' import { init } from 'snabbdom/build/package/init' // 2. div 中放置子元素 h1, p let patch = init([]) let vnode = h('div#container', [ h('h1', 'hello snabbdom'), h('p', 'this is a label') ]) const 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) // 清空页面元素 --- 错误 // patch(oldVnode, null) // 清空页面元素 --- 正确 patch(oldVnode, h('!')) }, 2000)
-
-
模块
snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要的话,可以使用模块
- attributes 特性模块
- 设置 DOM 元素的属性,使用 setAttributes
- 处理布尔类型的属性
- props 属性模块
- 和 attribute 模块相似,设置 DOM 元素的属性 element[attr] = value
- class 类模块
- 切换类样式
- 注意:给元素设置类样式是通过 sel 选择器
- dataset 数据集模块
- 设置 data-* 的自定义属性
- eventlisteners 事件监听器模块
- 注册和移除事件
- style 样式模块
- 设置行内样式,支持动画
- delayed/remove/destroy
- attributes 特性模块
-
模块使用
- 导入需要的模块
- init() 中注册模块
- 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
03-modules.js import { h } from 'snabbdom/build/package/h' import { init } from 'snabbdom/build/package/init' // 1. 导入模块 import { styleModule } from 'snabbdom/build/package/modules/style' import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners' // 2. 注册模块 let patch = init([ styleModule, eventListenersModule ]) // 3. 使用 h() 函数的第二个参数传入模块所需要的数据(对象) let vnode = h('div', { style: { backgroundColor: 'pink' }, on: { click: evnetHandler } }, [ h('h1', 'hello snabbdom'), h('p', 'this is a p label') ]) function evnetHandler () { console.log('click me !') } const app = document.querySelector('#app') patch(app, vnode)