Virtual DOM(虚拟 DOM)
Posted X可乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Virtual DOM(虚拟 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)
- 使用 Virtual DOM 来描述真实 DOM
sel: 'div', data: , children: undefined, text: 'hi Virtual DOM', 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)
- 通过模块可扩展
- 源码使用 TS 开发
- 最快的 Virtual DOM 之一
- virtual-dom
Snabbdom 基本使用
安装 parcel
md snabbdom-demo
cd snabbdom-demo
npm init -y
npm install parcel-bundler -D
配置 scripts
"scripts":
"dev": "parcel index.html --open",
"build": "parcel build index.html"
目录结构
- snabbdom-demo
- index.html
- package.json
- src(文件目录)
- 01-basicusage.js
导入 Snabbdom
Snabbdom 文档
- 看文档的意义
- 学习任何一个库都要先看文档
- 通过文档了解库的作用
- 看文档中提供的实例,自己快速实现一个 demo
- 通过文档查看 API
- Snabbdom 文档
- https://github.com/snabbdom/snabbdom
- 当前版本 v2.1.0
- 安装 Snabbdom
- npm i 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/package/init' import h from 'snabbdom/build/package/h'
- parcel/webpack 4 不支持 package.json 中的 exports 字段
- Snabbdom 的两个核心函数 init 和 h()
基本使用
import init from 'snabbdom/build/package/init'
import h from 'snabbdom/build/package/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)
/*
patch 接收两个参数
1. 旧节点
2. 新节点
存在多个 vNode 状态的时候可以做新旧节点对比
参数1 也可以传 DOM,传入 DOM 时,会先将 DOM 转换为 vNode,然后在做对比
最后返回更新以后的 vNode 值
*/
// 4. 创建新的 VNode 更新给 oldVNode
vNode = h('p#text.ptext', '这是 P 标签的内容')
patch(oldVNode, vNode)
包含子节点
import init from 'snabbdom/build/package/init'
import h from 'snabbdom/build/package/h'
const patch = init([])
// 创建包含子节点的 VNode
let vNode = h('div#box', [
h('h1', '标题文本'),
h('p', '内容文本')
])
/*
h 函数 的第二个参数为字符串时,直接作为当前创建元素的内容
如果是数组,代表传入的是子节点的列表,内部应该传入 vNode
*/
// 获取挂载元素
const dom = document.querySelector('#app')
// 渲染 vNode
const oldVNode = patch(dom, vNode)
// 生成一个注释节点,替换当前节点,用来清空节点
patch(oldVNode, h('!'))
Snabbdom 中的模块
模块的作用
- Snabbdom 的核心库并不能处理 DOM 元素的属性、样式、事件等,可以通过注册 Snabbdom 默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展 Snabbdom 的功能
- Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
官方提供的模块
- attributes
- props
- dataset
- 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. 注册模块(为 patch 函数添加模块对应的能力)
const patch = init([
styleModule,
eventListenersModule
])
let vNode = h('div#box',
style:
width: '200px',
height: '200px',
lineHeight: '100px',
textAlign: 'center',
backgroundColor: 'green'
,
on:
click()
console.log('div被点击了')
, [
h('h1.text',
style:
color: 'white'
,
on:
click()
console.log('h1被点击了')
,'标题文本'),
h('p.text', '标题文本')
])
const dom = document.querySelector('#app')
patch(dom, vNode)
Snabbdom 源码解析
- 如何学习源码
- 宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
- Snabbdom 的核心
- init() 设置模块,创建 patch() 函数
- 使用 h() 函数创建 JS 对象(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
h 函数
- 作用:创建 VNode 对象
- Vue 中的 h 函数
new Vue(
router,
store,
render: h => h(App)
).$mount('#app')
- 函数重载
- 参数个数或参数类型不同的函数
- JS 中没有重载的概念
- TS 中有重载,不过重载的实现还是通过代码调整参数
- 函数重载—参数个数
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')
patch 整体过程分析
- patch(oldVnode, newVnode)
- 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次
处理的旧节点 - 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化
Diff 算法
- 虚拟 DOM 中的 Diff 算法
- 查找两颗树每一个节点的差异
- 查找两颗树每一个节点的差异
- Snabbdom 根据 DOM 的特点对传统的 diff 算法做了优化
- DOM 操作时候很少会跨级别操作
- 只比较同级别的节点
执行过程
- 在对开始和结束节点比较的时候,总共有四种情
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
新旧开始节点
- 如果新旧开始节点是 sameVnode (key 和 sel 相同)
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++
旧开始节点 / 新结束节点
- 调用 patchVnode() 对比和更新节点
- 把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引
旧结束节点 / 新开始节点
- 调用 patchVnode() 对比和更新节点
- 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新索引
非上述四种情况
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
- 如果没有找到,说明 newStartNode 是新节点
- 创建了新节点对应的 DOM 元素,插入到 DOM 树种
- 如果找到了
- 判断新节点和找到的老节点的 sel 选择器是否相同
- 如果不相同,说明节点被修改了
- 重新创建对应的 DOM 元素,插入到 DOM 树中
- 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
循环结束
- 当老节点的所有子节点先遍历完(oldStartIdx > oldEndIdx)循环结束
- 新节点的所有子节点先遍历完(newStartIdx > newEndIdx)循环结束
oldStartIdx > oldEndIdx
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
- 说明新节点有剩余,把剩余节点批量插入到右边
- 说明新节点有剩余,把剩余节点批量插入到右边
newStartIdx > newEndIdx
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
- 说明老节点有剩余,把剩余节点批量删除
以上是关于Virtual DOM(虚拟 DOM)的主要内容,如果未能解决你的问题,请参考以下文章
虚拟DOM(Virtual Dom) VS 影子DOM(Shadow Dom)