虚拟DOM作用以及Virtual DOM库 Snabbdom

203 阅读4分钟

前言

最近研究了 虚拟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 操作时候很少会跨级别操作节点
    • 只比较同级别的节点
    比较同级别的节点.png

执行过程

  • 在对开始和结束节点比较的时候,总共有四种情

    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点
    • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点
    diff算法执行过程.png

开始和结束节点

  • 如果新旧开始节点是 sameVnode
    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx+
    开始、结束节点.png

旧开始节点 / 新结束节点

  • 调用 patchVnode() 对比和更新节点
  • 把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引
旧开始节点、新结束节点.png

旧结束节点 / 新开始节点

  • 调用 patchVnode() 对比和更新节点
  • 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新索
旧结束节点、新开始节点.png

非上述四种情况

非上述四种情况.png
  • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
  • 如果没有找到,说明 newStartNode 是新节点
    • 创建新节点对应的 DOM 元素,插入到 DOM 树中
  • 如果找到了
    • 判断新节点和找到的老节点的 sel 选择器是否相同
    • 如果不相同,说明节点被修改了
      • 重新创建对应的 DOM 元素,插入到 DOM 树中
    • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

循环结束

  • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx), 循环结束
  • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循 环结束

oldStartIdx > oldEndIdx

  • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
    • 说明新节点有剩余,把剩余节点批量插入到右边 旧开始节点大于旧结束节点.png

newStartIdx > newEndIdx

  • 如果新节点的数组先遍历完(newStartIdx > newEndIdx)

    • 说明老节点有剩余,把剩余节点批量删除
    新开始节点大于新结束节点.png

updateChildren

调试 updateChildren

调试带 key 的情况

节点对比过程

Key 的意义

有不同的意见,欢迎评论区讨论!