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'
        

基本使用

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)
  • 说明老节点有剩余,把剩余节点批量删除
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值