虚拟DOM 之 Snabbdom 一、基本介绍

Snabbdom

接口介绍([email protected]

官方文档

当前snabbdom版本为 @1.0.1。接口介绍在官方文档的基础上做扩展,新版本接口使用基本和@0.7.4差不多。

Snabbdom的核心只提供最基本的功能。更多功能可以通过“模块”扩展。

Snabbdom用于扩展的“模块”,类似于插件。可以自定义。

导入

官方提供的导入snabbdom的示例是:

import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h'

但目前在parcel运行时会报错,找不到对应模块,查看node_modules/snabbdom/package.json配置,找到一个exports字段(可能是新字段,npmjs官方没有介绍该字段)。

exports字段中配置了snabbdom下每个模块的路径,直接使用这个路径导入即可。

import { init } from 'snabbdom/build/package/init'
import { classModule } from 'snabbdom/build/package/modules/class'
import { propsModule } from 'snabbdom/build/package/modules/props'
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
import { h } from 'snabbdom/build/package/h'

init()

Snabbdom核心只公开了一个init函数。(不懂官方对“核心”的定义)

init 的作用就是创建一个使用指定模块集的patch(补丁)函数。

语法:init: (modules: Array[Module]) => patch:Function

参数:

  • modules(必选):一个数组,存放需要注册的模块,默认必须传个[]
    • 模块指的是扩展Snabbdom功能的模块(插件),可以是内置模块,或自定义的模块。
  • domApi(可选):操作虚拟DOM的API对象
    • 默认是snabbdom封装好的一些方法,包括:
      • 将虚拟DOM转化为真实DOM
      • 删除、添加、插入DOM等操作
    • 可以通过传递 domApi,传入一些自定义操作,把虚拟DOM转化为具体想要的内容
      • 如 HTML字符串 或 其他类型的内容
    • 具体使用需要查看源码中init()函数中对domApi的使用

返回:

  • patch:一个使用指定模块集的补丁函数
import { init } from 'snabbdom/build/package/init'
import { classModule } from 'snabbdom/build/package/modules/class'
import { propsModule } from 'snabbdom/build/package/modules/props'
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'

// 不使用模块,必须传个空[]
// var patch = init([])

var patch = init([
  classModule, // 切换类的模块
  propsModule, // 设置DOM元素属性的模块
  styleModule, // 设置行内样式或动画的模块
  eventListenersModule, // 事件监听模块
])

patch()

patch 的作用就是对比新旧两个vnode的差异,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点。

它是整个snabbdom中的核心函数。

语法:patch: (oldVnode: Element | VNode, newVnode: VNode) => VNode: newVnode

参数:

  • oldVnode:接收一个真实的DOM 或 一个vnode
    • 真实的DOM:首次调用patch时往往传入页面上的一个占位符,例如Vue的el选项(#app)表示的DOM对象。
      • 第一个参数是真实DOM的情况,会将它转化成一个空的Vnode,然后经过判断新旧vnode不相同(key和sel相同),直接把newVnode转化成真实DOM,插入DOM树中,再从DOM树种移除参数1传入的DOM元素。
      • 所以这个情况不会去对比两个Vnode的差异
    • oldVnode:旧的VNode,它应该是上一次patch返回的结果。
      • 虽然使用手动获取的真实DOM,或历史用过的VNode,也能实现替换。但是应该使用最近一次的VNode去对比。
      • 因为snabbdom已经将信息存储在VNode中,所以为保证对比差异更少更精确,使用最近一次的结果去对比,避免进行多余的没有意义的对比、更新甚至创建新的用于对比的VNode,才是Snabbdom的使用目的。
  • newVnode:表示更新后的新视图的VNode
    • 它是VNode对象,而不是真实DOM对象

返回:

  • newVnode

卸载DOM官方方案:用一个【注释】节点作为newValue替换oldVnode,实现视觉上卸载DOM的效果。

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

let patch = init([])
let element = document.querySelector('#app')
let newValue = h('div', 'Hello world')

let oldValue = patch(element, newValue)
console.log(oldValue === newValue) // true

// oldValue应该是上一次patch返回的结果
let endVnode = patch(oldValue, h('div', 'Hello Snabbdom'))

// 卸载DOM
let commentsVnode = patch(endVnode, h('!'))

h()

Snabbdom建议使用h 函数创建描述真实DOM的Virtual DOM(VNode)。

语法:h: (sel: string, [data: VNodeData || null], [children: VNodeChildren]) => VNode

参数:

h 函数可以接收3个参数:

  • sel(必选):必须包含标签的名的css选择器 或 表示注释的!
    • css选择器由两个内容组成:
      • 标签名(必选):divh1p
      • id/class选择器(可选):#app.cls
        • id选择器必须在class前面,因为snabbdom内部解析sel的时候,默认把id选择器当作最前面的去解析。[详细查看源码解析-createElm()函数](#createElm() 函数)
  • data(可选):VNode数据对象 VNodeData,用来设置sel表示的标签的内容、属性、样式、事件绑定、key、hook等
    • 需要注册对应模块(init)才能在patch时生效,例如注册 styleModule 模块 style 样式才会生效
  • children(可选):子节点内容,允许接收的类型:
    • String:字符串会转化成文本节点
    • Number:数字也会转化成文本节点
    • VNode:使用h函数创建的VNode
    • Array:包含多个子节点的数组,子节点类型可以是 String Number VNode

data 和 children可选参数可以单独使用,也可以同时使用,同时使用时,data在children前面。

使用方式示例汇总:

h('div') // h(sel)
h('div', 'Hello world') // h(sel, children)
h('div', { style: '#333' }) // h(sel, data)
h('div', { style: '#333' }, 'Hello World') // h(sel, data, children)

基本使用

创建项目

打包工具为了方便使用 parcel

# 在项目目录下创建package.json
npm init -y
# 本地安装parcel
npm install parcel

配置 package.json 的scripts

{
  "scripts": {
    "dev": "parcel inxex.html --open",
    "build": "parcel build index.html"
  }
}

创建目录结构

│	index.html # parcel的入口文件
│ package.json
└-src
		01-basicusage.js

导入[email protected]

ESM 导入

安装[email protected](注意代码演示中使用不是新版本@1.x)

npm install [email protected]

官方示例使用的是CommonJS导入的snabbdom,如果用ES Module形式直接导入会返回undefined。

通过查看node_modules/snabbdom中的源码发现,当使用ESM的时候,导入的是es/snabbdom.js,即src/snabbdom.ts编译后的文件。

可以查看它们之一的源码,搜索export,发现只导出了3个成员,并没有导出default默认成员。

使用CommonJS的require导入模块,会将所有成员打包成一个对象。

使用ESM直接导入模块,只能接收默认成员,所以直接导入获取不到。

ESM也可以通过*接收全部成员。

// 官方示例使用CommonJS方式导入
// var snabbdom = require('snabbdom');

// 使用ESM方式导入 snabbdom

// ESM方式导入的是node_modules/ex/snabbdom.js文件
// 该文件只道导出了 h thunk init 3个具名成员,并没有导出默认成员
// 所以直接导入接收不到
// import snabbdom from 'snabbdom'
// console.logo(snabbdom) // undefined

// 导入时指定接收的成员
import { h, thunk, init } from 'snabbdom'
console.log(h, thunk, init)

// 导入时指定一个对象名,用于接收全部成员
// import * as snabbdom from 'snabbdom'
// console.log(snabbdom)

Snabbdom 的核心仅提供最近本的功能,只导出了三个函数:

  • init() 是一个高阶函数,返回patch()
  • h() 返回虚拟节点 VNode,Vue的render中使用了这个函数
    • h()函数用于创建虚拟DOM,在Snabbdom中用VNode描述虚拟节点,也就是虚拟DOM。
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  • thunk() 是一种优化策略,可以在处理不可变数据时使用(用于优化复杂的视图)

模块化语法参考

模块化语法可以参考阮一峰老师的文章:

  • Module 的语法
  • ES6-模块与-CommonJS-模块的差异

代码演示

演示功能:

  1. hello world
  2. div中放置子元素:h1 p

流程:

  1. 首先使用init()创建patch函数
  2. 然后使用h()创建VNode(虚拟DOM)

功能1代码:

// 代码演示
import { h, init } from 'snabbdom'

// 1. hello world

let patch = init([])

let vnode = h('div#container.cls', 'hello world')

// 获取占位DOM
let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)

// 对比差异更新视图
// 假设有个操作要更新DOM
setTimeout(() => {
  vnode = h('div', 'Hello Snabbdom')
  patch(oldVnode, vnode)
}, 1000)

功能2代码:

// 2. div中放置子元素 h1 p
import { h, init } from 'snabbdom'

let patch = init([])

let vnode = h('div#container.cls', [
  h('h1', 'Hello Sabbdom'),
  h('p', '这是一个p标签')
])

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

let oldVnode = patch(app, vnode)

// 对比差异更新视图
// 假设有个操作要更新DOM
// 更新DOM子元素
setTimeout(() => {
  vnode = h('div#container.cls', [
    h('h1', 'Hello world'),
    h('p', 'Hello p')
  ])
  vnode = patch(oldVnode, vnode)

  // 删除DOM(官方示例是个错误示例,已经被删掉)
  // 报错:Cannot read property 'key' of null
  // patch(endVnode, null)

  // 通过创建注释节点来实现
  // vnode = patch(vnode, h('!'))
  
  // Vnode节点仍然存在
  // patch(vnode, h('div', '又在原位置出现'))
}, 1000)

删除DOM节点的方法

Snabbdom 旧版本中的官方文档曾经介绍卸载DOM的方法:

patch(old, null)

但是这个方式并不成功。

然后有网友提出了issues:

Unmounting by patch(vnode, null) documented but not implemented #461

其中另一个网友提出可以通过用一个注释节点,替换旧节点实现卸载DOM。

最终Snabbdom作者更新了文档:

Unmounting / 卸载

While there is no API specifically for removing a VNode tree from its mount point element, one way of almost achieving this is providing a comment VNode as the second argument to patch, such as:

虽然没有专门用于用挂载元素中删除VNode树的API。

但可以通过提供注释VNode作为patch的第二个参数的方法,几乎实现这一点。

例如:

patch(oldVnode, h('!', { hooks: { post: () => { /* patch complete */ } } }))

当然,挂载元素上仍然会有一个注释节点()。

注意虽然使用这个方法删除了DOM,好像页面中找不到原来的位置。

但其实patch返回的vnode仍然记录了信息,因为它返回了一个注释节点。

重新用它更新视图,依然能在原位置渲染。

模块

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

常用模块

官方提供了 6 个模块,也可以自定义模块。

  • attributes
    • 设置DOM元素的属性
    • 内部使用 DOM setAttribute() 方法
    • 处理布尔类型的属性时也会做相应的判断 selected checked等
  • props
    • 和 attributes 模块类似,设置DOM元素的属性
    • 内部使用的是 element[attr] = value 方式
    • 并且不会处理布尔类型的属性
  • class
    • 用于 切换 类样式
    • 注意:设置 元素的类样式是通过 sel 选择器
  • dataset
    • 设置 HTML5 中的 data-* 的自定义属性
  • eventlisteners
    • 注册和移除事件
  • style
    • 设置行内样式,支持动画(内部创建transitionend事件)
    • 额外的属性:delayed / remove / destroy

模块使用

步骤:

  1. 导入模块
  2. init() 中注册模块
  3. 使用 h() 函数创建VNode的时候,第二个参数可以传对象,对象中是模块需要的数据,可以设置行内样式、事件等
import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'

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

// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
  style: {
    backgroundColor: 'red'
  },
  on: {
    click: eventHandler
  }
}, [
  h('h1', '这是h1标签'),
  h('p', '这是p标签')
])

function eventHandler(event) {
  console.log('点击触发:',event.target.textContent)
}

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

你可能感兴趣的:(前端基础)