官方文档
当前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'
Snabbdom核心只公开了一个init函数。(不懂官方对“核心”的定义)
init 的作用就是创建一个使用指定模块集的patch(补丁)函数。
语法:init: (modules: Array[Module]) => patch:Function
参数:
[]
返回:
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 的作用就是对比新旧两个vnode的差异,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点。
它是整个snabbdom中的核心函数。
语法:patch: (oldVnode: Element | VNode, newVnode: VNode) => VNode: newVnode
参数:
#app
)表示的DOM对象。
返回:
卸载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('!'))
Snabbdom建议使用h 函数创建描述真实DOM的Virtual DOM(VNode)。
语法:h: (sel: string, [data: VNodeData || null], [children: VNodeChildren]) => VNode
参数:
h 函数可以接收3个参数:
!
div
、h1
、p
等#app
、.cls
等
VNodeData
,用来设置sel
表示的标签的内容、属性、样式、事件绑定、key、hook等
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](注意代码演示中使用不是新版本@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 的核心仅提供最近本的功能,只导出了三个函数:
VNode
描述虚拟节点,也就是虚拟DOM。new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
模块化语法可以参考阮一峰老师的文章:
演示功能:
流程:
// 代码演示
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. 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)
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 个模块,也可以自定义模块。
element[attr] = value
方式sel
选择器data-*
的自定义属性步骤:
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)