虚拟DOM
什么是 Virtual DOM
- Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
- 真实 DOM 成员
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
}
- 可以使用 Virtual DOM 来描述真实 DOM,示例
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}
为什么使用 Virtual DOM
- 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
- 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
- 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了
- Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等
虚拟DOM库:snabbdom
来了解一下虚拟dom库snabbdom吧!
- 首先安装
// 创建 package.json
npm init -y
// 安装 parcel、snabbdom
npm install parcel-bundler -D
npm install snabbdom@2.1.0
- 配置package.json
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
}
- 初步使用
// snabbdom中的模块:
/*
1、模块的作用:Snabbdom的核心模块并不能处理DOM元素的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现;Snabbdom中的模块可以用来扩展Snabbdom的功能;Snabbdom中的模块的实现是通过注册全局的钩子函数来实现的。
2、官方提供的模块:attributes、props、dataset、class、style、eventlisteners
3、使用的步骤:导入需要的模块;init()注册模块;h()函数的第二个参数处使用模块
4、snabbdom的核心:init()设置模块,创建patch()函数;使用h()函数创建JavaScript对象(vnode)描述真实DOM;patch()比较新旧两个VNode;把变化的内容更新到真实DOM树。
*/
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 如果是webpack5.x的版本打包的话可以直接这么导入snabbdom:import { init } from 'snabbdom/init'
// import { h } from 'snabbdom/h'
// patch 可以把虚拟dom转换为真实DOM;并且挂载到DOM树上
// 有两个参数;都是vnode;第一个参数如果是真实DOM;会转为VNode然后在做比较;然后返回新的VNode
const patch = init([])
// h函数的两个参数:第一个参数(字符串)是标签+选择器;第二个参数如果是字符串的话,代表的是文本内容
// vnode描述的真实DOM
let vnode = h('div#container.cle', '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)
// 清除div中的内容 h('!')生成的就是一个空的注释节点
// patch(oldVnode, h('!'))
diff算法
// 虚拟DOM中的Diff算法:查找两棵树每一个节点的差异
// 执行过程:在对开始和结束节点比较的时候:总共有四种情况:
// ----- oldStartVnode/ newStartVnode(旧开始节点/新开始节点);
// ----- oldEndVnode/ newEndVnode(旧结束节点/新结束节点);
// ----- oldStartVnode/ newEndVnode(旧开始节点/新结束节点);
// ----- oldEndVnode/ newStartVnode(旧结束节点/新开始节点);
// 开始和结束节点:如果新旧开始节点是sameVnode(key和sel相同),调用patchVnode()对比和更新节点,把旧开始和新开始索引往后移动,oldStartIdx++/ newStartIdx++,再去比较第二个节点,把第二个节点作为开始节点开始比较;如果不是sameVnode则会比较最后一个旧结束节点和新结束节点是否是sameVnode;相同则移动到倒数第二个节点作为结束节点进行比较。
// 旧开始节点和新结束节点:比较两个节点;如果是sameVNode就调用patchVnode()对比和更新节点,并且把oldStartVnode对应的DOM元素移动到最后,更新索引,始终保证第一个节点就是旧开始节点,新结束节点往前移动一位,接着进行比较两个节点
// 旧结束节点和新开始节点: 比较两个节点;如果是sameVNode就调用patchVnode()对比和更新节点,并且把oldEndVnode对应的DOM元素移动到最前面,更新索引,始终保证最后一个节点就是旧结束节点,新开始节点往后移动一位,接着进行比较两个节点
// 如果以上四种情况都不满足:说明开始和结束节点都不相同。就需要从旧节点中查找新节点(遍历新节点,查找是否在旧节点中有相同key和sel值得节点;若没有,创建新的DOM元素,并插入到最前面的位置;若有;把找到的旧节点赋值给elmToMove这个变量,调用parchVnode比较两个节点,和更新节点差异,再把elmToMove对于的DOM元素移动到最前面) 。
// 循环结束:1、当老节点的所有子节点先遍历完,循环结束(oldStartIdx > oldEndIdx) 2、新节点的所有子节点先遍历完,循环结束 (newStartIdx > newEndIdx)
// diff算法过程:
/*
-- patch(oldVnode,newVnode)
-- 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点。
-- 对比新旧VNode是否相同节点(节点的key和sel相同)
-- 如果不是相同节点,删除之前的内容,重新渲染
-- 如果是相同节点,在判断新的VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容
-- 如果新的VNode有children,判断子节点是否有变化
-- diff过程只进行同层级比较,时间复杂度O(n)
*/