虚拟DOM库Snabbdom源码解析

717 阅读3分钟

报告:Vue.js内部当数据变化后,操作的是虚拟DOM,会对比新旧两次虚拟DOM的差异,再把差异更新到真实DOM

虚拟DOM库Snabbdom源码解析

虚拟DOM如何工作的?

Virtual DOM虚拟DOM:由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫Virtual DOM

{
    sel: 'div',
    data: {},
    children: undefined,
    text: '1',
    elm: undefined,
    key: undefined
}

为什么使用?

  • 手动操作DOM麻烦,还需要考虑浏览器兼容性问题,虽有jQuery等简化DOM操作,随着项目的复杂DOM操作复杂提升
  • 为了简化DOM复杂,有了MVVM框架,解决了视图和状态的同步问题
  • 使用模板引擎简化视图,但是它没有解决跟踪状态变化问题,于是出现了Virtual DOM
  • Virtual DOM好处是当状态改变不需要立即更新DOM,创建一个虚拟树来描述DOM,Virtual DOM内部将弄清楚如何有效(diff)的更新DOM

描述:

  • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
  • 通过比较前后两次状态的差异更新真实DOM

作用

  1. 维护视图和状态的关系

  2. 复杂视图情况下提升渲染性能

  3. 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uin-app)等

Virtual DOM库

  • Snabbdom
  • virtual-dom

创建项目

  • 打包工具:parcel
  • 创建项目,并安装parcel
yarn init -y
yarn add parcel-bundler
  • 配置package.jsonscripts
"scripts": {
	"dev": "parcel index.html --open",
	"build": "parcel build index.html"
}
  • 创建目录结构
index.html
package.json
-src
	main.js

导入Snabbdom

  • Snabbdom导入使用commonjs模块语法,流行使用ES6模块化语法import
  • ES6模块与CommonJS模块的差异
yarn add snabbdom
import { h, thunk, init } from 'snabbdom'
  • Snabbdom核心仅提供最基本的功能,只导出了三个函数init()、h()、thunk()

    • init()是一个高阶函数,返回patch()

    • h()返回虚拟节点VNode,这个函数我们在使用Vuejs的时候见过

      new Vue({
          router,
          store,
          render: h => h(App)
      }).$mount('#app')
      
    • thunk()是一种优化策略,可以在处理不可变数据时使用

  • 注意:导入不能使用import snabbdom from 'snabbdom'

    • 原因:末尾到处使用的语法时export到处API,没有使用export default导出默认输出

      export {h} from './h'
      export {thunk} from './thunk'
      
      export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI){
      }
      

Snabbdom代码演示

  1. 1.hello world
//main.js
import {h, init} from 'snabbdom'

/**
* 1.hello world
* 参数:数组、模块
* 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
*/
let patch = init([])
/**
* 第一个参数:标签+选择器
* 第二个参数:如果是字符串的话就是标签中内容
*/
let vnode = h('div#container.cls', 'hello world')

let app = document.querySelector('#app')
/**
* 第一个参数:可以是DOM元素,内部会把DOM元素转换为VNode
* 第二个参数:VNode
* 返回值:VNode
*/
let oldVnode = patch(app, vnode)

//假设的时刻:覆盖之前的元素
vnode = h('div', 'hello snabbdom')

patch(oldVnode, vnode)
  1. div中放置子元素h1,p
import { h, init } from 'snabbdom'

let patch = init([])

let vnode = h('div#contatainer', [
    h('h1', 'hello'),
    h('p', 'p标签')
])

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)

setTimeout(() => {
    vnode = h('div#contatainer', [
    h('h1', '22222'),
    h('p', '11111')
	])
    patch(oldVnode, vnode)
    
    //清空页面元素-错误:无法读取null的key属性
    //patch(oldVnode, null)
    patch(oldVnode, h('!'))
},2000)
<script src="./src/main.js"></script>
<!--要引入-->

模块

  • snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
  • 常用模块:官方提供了6个模块
    1. attributes
      • 设置DOM元素属性,使用setAttribute()
      • 处理布尔类型的属性
    2. props
      • attributes模块相似,设置DOM元素的属性element[attr] = value
      • 不处理布尔类型的属性
    3. class
      • 切换类样式
      • 注意:给元素设置类样式是通过sel选择器
    4. dataset
      • 设置data-*的自定义属性
    5. eventlisteners
      • 注册和移除事件
    6. style
      • 设置行内样式,支持动画
      • delayed/remove/destroy

模块使用

  • 模块使用步骤:
    • 导入需要的模块
    • init()中注册模块
    • 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其它参数往后移
  • 代码
//main.js
import { init, h } from 'snabbdom'

// 1.导入需要的模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'

// 2.init()中注册模块
let patch = init([
    style,
    eventlisteners
])

// 3.使用h()函数的第二个参数传入模块需要的数据(对象)
/**以前的h()
* let vnode = h('div#contatainer', [
*     h('h1', 'hello'),
*     h('p', 'p标签')
* ])
*/

let vnode = h('div', {
    style: {
        backgroundColor: 'red'
    },
    on: {
        click: eventHandler
    }
},[
    h('h1','hello!'),
    h('p','这是p')
])
function eventHandler() {
    console.log('1')
}

let app = document.quertSelector('#app')
patch(app, vnode)

Snabbdom源码解析

Snabbdom核心

  • 使用h()函数创建JavaScript对象(VNode)描述真实DOM
  • init()设置模块,创建patch()
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实DOM树上

h()

  • Vue中的h()函数支持组件机制,但Snabbdom不支持,但一样可以传入选择器

    new Vue({
        router,
        store,
        render: h => h(App)
    }).$mount('#app')
    
    • h()最早适用于hyperscript,使用JavaScript创建超文本
    • Snabbdom中h()不是创建超文本,而是创建VNode
  • 函数重载

    1. 参数个数类型不同的函数
    2. JavaScript中没有重载的概念
    3. typescript中有重载,不过重载的实现还是通过代码调整参数
  • 可以定义两个重名函数,通过参数个数、类型不同来区分

  • 分析:h.ts中重载的几个h()函数

VNode

export interface VNode {
    //选择器
    sel: string | undefined;
    //节点数据:属性/样式/事件
    data: VNodeData | undefined;
    //子节点:和text只能互斥
    children: Array<VNode | string> | undefined;
    //记录vnode对应的真实DOM
    elm: Node | undefined;
    //节点中的内容,和children只能互斥
    text: string | undefined;
    //优化用
    key: Key | undefined;
}

VNode渲染真实DOM

patch(oldVnode, newVnode )

  • patch()打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点,作为下一次处理的旧节点
  • patchVnode():对比新旧节点VNode是否是相同节点
    • 若不是相同节点,删除之前的内容removeVnodes(),重新渲染
    • 如果是相同节点:
      • 判断新的VNode是否有text,如果有并且和oldVnode的text不同,setTextContent()直接更新文本内容
      • updateChildren():如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
  • diff过程只进行同层级比较

init函数

  • 返回一个patch(),是高阶函数

patch函数

  • 把vnode渲染成真实dom


本文首发于我的GitHub博客,其它博客同步更新。