vue --- Virtual DOM 的实现原理

306 阅读5分钟

Virtual DOM 的实现原理

1)Virtual DOM

  • Virtual DOM (虚拟 DOM ),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
  • 真实 DOM
let element = document.querySelector('#app')
let s = ''
for (const key in element) {
    s += key + ','
}
console.log(s)
  • 可以使用 Virtual DOM 来描述真实 DOM , 示例
{
    sel: 'div',
    data: {},
    children: undefined,
    text: 'heloo virtual dom',
    elm: undefined,
    key: undefined
}
  • 为什么使用 virtual DOM

    • 手动操作 DOM 笔记麻烦,还需要考虑浏览器兼容性问题
    • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图状态同步问题
    • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 virtual DOM 出现了
    • virtual DOM 的好处是当状态改变时不需要立即更新 DOM, 只需要创建一个虚拟树来描述 DOM, virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
    • 参考 github 上 virtual-dom 的描述
      • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
      • 通过比较前后两次状态的差异更新真实 DOM
  • 虚拟 DOM 的作用

    • 维护视图和状态的关系
    • 复杂视图情况下提升渲染性能
    • 处理渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

  • virtual DOM 库
    • Snabbdom
      • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
      • 大约 200 SLOC
      • 通过模块可扩展
      • 源码使用 TypeScript 开发
      • 最快的 Virtual DOM 之一
    • virtual-dom

2)Snabbdom 基本使用

snabbdom 官网

Snabbdom 官方文档翻译

  • 创建项目

    • 打包工具为了方便使用 parcel
    • 创建项目,并安装 parcel
    md snabbdom-dom
    
    cd snabbdom-dom
    
    yarn init -y
    
    yarn add parcel-bundler --dev
    
    • 配置 package.json 的 scripts
      "scripts": {
        "dev": "parcel index.html --open",
        "build": "parcel build index.html"
      },
    
    • 创建目录结构
    | index.html
    | package.json
    |_src
    	| 01-basicusage.js
    
  • 导入项目

    • 安装
    yarn add snabbdom
    
    • "snabbdom": "^2.1.0" (与低版本使用 0.7.4 有区别)
    import { h } from 'snabbdom/build/package/h'
    import { thunk } from 'snabbdom/build/package/thunk'
    import { init } from 'snabbdom/build/package/init'
    

    注意

    导入的时候不能使用 import snabbdom from 'snabbdom', 因为 `node_modules/snabbdom/src/package/*.ts 末尾导出使用的语法是 **export 导出 API **,**没有使用 export default 导出默认输出 **

  • snabbdom 的核心仅提供最基本的功能

    • snabbdom.init 核心仅暴露出一个函数snabbdom.initinit接收一个模块列表,并返回一个使用指定模块集的patch函数。
    • snabbdom/h 建议使用snabbdom/h创建虚拟节点(vnodes)。h函数接收一个字符串形式的标签/选择器、一个可选的数据对象、一个可选的字符串或数组作为子代。
    • thunk 是一个优化策略,可用于处理不可变数据
  • 基本案例

    • patch

      init返回的patch函数有两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示更新后的新视图的vnode。

      如果传递带有父节点的DOM元素,newVnode将被转换为DOM节点,传递的元素将被创建的DOM节点替换。如果传递旧的vnode, Snabbdom将有效地修改它以匹配新vnode中的描述。

      传递的任何旧vnode都必须是上一个patch调用的结果vnode。这是必要的,因为Snabbdom将信息存储在vnode中。这使得实现更简单、更高性能的体系结构成为可能。这也避免了创建新的旧vnode树。

    // 01-basicusage.js
    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    // import { thunk } from 'snabbdom/build/package/thunk'
    // import { classModule } from 'snabbdom/build/package/modules/class'
    
    // console.log(h, thunk, init, classModule)
    
    // 1. 实现一个 hello world
    // init 参数:数组,模块   返回值:patch函数,作用对比两个 VNode 的差异更新到真实 DOM
    let patch = init([])
    
    // h  参数(标签+选择器,如果是字符串的话就是标签中的内容)
    let vnode = h('div#container.cls', 'hello world')
    
    let app = document.querySelector('#app')
    
    // patch函数有两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示更新后的新视图的vnode。
    // 第一个参数:可以是 DOM 元素,内部会把 DOM 元素转换成 VNode
    // 第二个参数:VNode
    // 返回值:VNode
    let oldVnode = patch(app, vnode)
    
    // 更新数据
    let newVnode = h('div', 'hello snabbdom')
    patch(oldVnode, newVnode)
    
    
    // 02-basicusage.js
    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    
    // 2. div 中放置子元素 h1, p
    
    let patch = init([])
    
    let vnode = h('div#container', [
        h('h1', 'hello snabbdom'),
        h('p', 'this is a label')
    ])
    
    const app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    // 更新数据
    setTimeout(() => {
        vnode = h('div#container', [
            h('h1', 'hello world'),
            h('p', 'hello p')
        ])
        patch(oldVnode, vnode)
        
        // 清空页面元素 --- 错误
        // patch(oldVnode, null)
    
        // 清空页面元素 --- 正确
        patch(oldVnode, h('!'))
    }, 2000)
    
  • 模块

    snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要的话,可以使用模块

    • attributes 特性模块
      • 设置 DOM 元素的属性,使用 setAttributes
      • 处理布尔类型的属性
    • props 属性模块
      • 和 attribute 模块相似,设置 DOM 元素的属性 element[attr] = value
    • class 类模块
      • 切换类样式
      • 注意:给元素设置类样式是通过 sel 选择器
    • dataset 数据集模块
      • 设置 data-* 的自定义属性
    • eventlisteners 事件监听器模块
      • 注册和移除事件
    • style 样式模块
      • 设置行内样式,支持动画
      • delayed/remove/destroy
  • 模块使用

    • 导入需要的模块
    • init() 中注册模块
    • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
    03-modules.js
    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    
    // 1. 导入模块
    import { styleModule } from 'snabbdom/build/package/modules/style'
    import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
    
    // 2. 注册模块
    let patch = init([
        styleModule,
        eventListenersModule
    ])
    
    // 3. 使用 h() 函数的第二个参数传入模块所需要的数据(对象)
    let vnode = h('div', {
        style: {
            backgroundColor: 'pink'
        },
        on: {
            click: evnetHandler
        }
    }, [
        h('h1', 'hello snabbdom'),
        h('p', 'this is a p label')
    ])
    
    function evnetHandler () {
        console.log('click me !')
    }
    
    const app = document.querySelector('#app')
    
    patch(app, vnode)