前言
最近研究了 虚拟DOM 一段时间,想想觉得还是要输出点东西,我们先来从为什么要使用 Virtual DOM 和 虚拟 DOM 的作用 来展开说。
虚拟 DOM 是由普通的JS对象来描述DOM对象;使用 Virtual DOM 来描述真实DOM
{
sel: "div",
data: {},
children: undefined,
text: "Hellow Virtual DOM",
elm: undefined,
key: undefined
}
使用 Virtual DOM的原因
- 前端开发刀耕火种的时代
- MVVM 框架解决视图和状态同步问题
- 模板引擎可以简化视图操作,没办法跟踪状态
- 虚拟 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 single line of code
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
-
virtual-dom
Snabbdom 基本使用
模块的作用
- Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等, 可以通过注册 Snabbdom 默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展 Snabbdom的功能
- Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
官方提供的模块
- attributes
- props
- dataset
- class
- style
- eventListeners
模块使用步骤
- 导入需要的模块
- init() 中注册模块
- h() 函数的第二个参数处使用模块
Snabbdom 源码解析
如何学习源码
- 宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
Snabbdom 的核心
- init() 设置模块,创建 patch() 函数
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- patch() 比较新旧两个 Vnode
- 把变化的内容更新到真实 DOM 树
h 函数介绍
- 作用:创建 VNode 对象
- Vue 中的 h 函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
patch 整体过程分析
- patch(oldVnode, newVnode)
- 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点时候变化
init 函数
patch 函数
调试 patch 函数
createElm
调试 createElm
removeVnodes 和 addVnodes
patchVnode
updateChildren 整体分析
Diff 算法
- 虚拟 DOM 中的 Diff 算法
- 查找两颗树每个节点的差异
- Snbbdom 根据 DOM 的特点对传统的diff算法做了优化
- DOM 操作时候很少会跨级别操作节点
- 只比较同级别的节点
执行过程
-
在对开始和结束节点比较的时候,总共有四种情
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点
- oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点
开始和结束节点
- 如果新旧开始节点是 sameVnode
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx+
旧开始节点 / 新结束节点
- 调用 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)
- 说明老节点有剩余,把剩余节点批量删除
updateChildren
调试 updateChildren
调试带 key 的情况
节点对比过程
Key 的意义
有不同的意见,欢迎评论区讨论!