Snabbdom 简介

模块机制:样式、属性与事件

理解 Snabbdom 的模块机制,掌握 class、style、props、attrs、dataset、事件六大常用模块的用法。

🎯 引言

上一篇创建的虚拟节点只描述了页面结构,还不能加样式、不能绑事件。这篇我们学习 Snabbdom 的模块机制,掌握六大常用模块,让虚拟节点拥有完整的页面表现力。


🧱 为什么需要模块

Snabbdom 的核心极其精简,只认识 seltextchildrenkey 这四样东西。那 class、样式、事件怎么办?答案是:全部做成模块,用哪个装哪个

这就像买手机:核心是一台裸机,拍照、游戏等能力按需安装 App。模块通过 init 注册:

main.js
import {
    init,
    h,
    classModule,
    propsModule,
    attributesModule,
    datasetModule,
    styleModule,
    eventListenersModule
} from 'snabbdom';

const patch = init([
    classModule, // class 模块
    propsModule, // DOM 属性模块
    attributesModule, // HTML 特性模块
    datasetModule, // data-* 模块
    styleModule, // style 模块
    eventListenersModule // 事件模块
]);

注册之后,h 函数的第二个参数 data 就能识别对应的字段了。没注册的模块,写了也不生效,这是新手最常见的坑。


🎨 class 模块与 style 模块

class 模块用对象控制类名,键是类名,值是布尔值:

main.js
const vnode = h(
    'div.card',
    {
        class: { active: true, hidden: false }
    },
    '卡片'
);

渲染结果:

<div class="card active">卡片</div>

更新时把 active 改成 false,Snabbdom 会精准地移除这个类,其他类不受影响。

style 模块用对象写行内样式,属性名用驼峰写法:

main.js
const vnode = h(
    'div',
    {
        style: { color: 'red', fontSize: '20px' }
    },
    '红色文字'
);

渲染结果:

<div style="color: red; font-size: 20px;">红色文字</div>
style 模块还支持过渡技巧 delayed(元素插入后延迟应用的样式),配合 CSS transition 可以实现入场动画,属于进阶用法,了解即可。

⚖️ props 和 attrs 的区别(重点)

这两个模块最容易混淆,必须分清:

模块data 字段操作方式体现在 HTML 标签上?
propsModuleprops直接设置 DOM 对象的属性不一定
attributesModuleattrs调用 setAttribute 设置 HTML 特性

看一组对比实验:

main.js
// props:设置 DOM 属性
h('input', { props: { value: 'hello' } });
// 渲染结果:<input>,标签上看不到 value,但输入框里确实有值

// attrs:设置 HTML 特性
h('input', { attrs: { value: 'hello', placeholder: '请输入' } });
// 渲染结果:<input value="hello" placeholder="请输入">

选型建议

  • 操作表单值(valuechecked)、disabled 这类 DOM 状态,用 props
  • 操作 placeholdertitledata-* 这类标签特性,用 attrs

📦 dataset 模块与事件模块

dataset 模块专门设置 data-* 自定义特性,键名会自动转成短横线风格:

main.js
h(
    'div',
    {
        dataset: { action: 'reset', userId: 100 }
    },
    '内容'
);
// 渲染结果:<div data-action="reset" data-user-id="100">内容</div>

事件模块on 字段绑定事件:

main.js
const vnode = h(
    'button',
    {
        on: {
            click: () => alert('被点击了')
        }
    },
    '点我'
);

最省心的是:更新或移除节点时,旧的事件监听器会被 Snabbdom 自动移除,不需要手动解绑,从根源上避免了内存泄漏。


🛠 综合示例

把几个模块组合起来,渲染一张带交互的卡片:

main.js
const vnode = h(
    'div.card',
    {
        class: { active: true },
        style: { backgroundColor: '#f5f5f5', padding: '20px' },
        dataset: { id: 1 },
        attrs: { title: '鼠标悬停提示' },
        on: { click: () => console.log('卡片被点击') }
    },
    '一张完整的卡片'
);

patch(document.getElementById('app'), vnode);

一个对象就描述清了结构、样式、数据与行为——这正是虚拟 DOM 的魅力。


🧾 小节总结

  • Snabbdom 核心极简,class、样式、事件等能力都通过模块按需注册。
  • 模块在 init([...]) 中注册,没注册的模块写了不生效。
  • class 模块用 { 类名: 布尔值 } 精确控制类名;style 模块用驼峰属性名写行内样式。
  • props 设置 DOM 属性,attrs 设置 HTML 特性:表单状态用 props,标签特性用 attrs。
  • dataset 设置 data-* 特性;事件用 on 绑定,旧监听器会被自动移除。

❓ 知识问答

Q1:没注册 style 模块就写了 style 字段,会报错吗?

A:不会报错,但样式完全不生效。页面表现和预期不符时,先检查模块是否注册。

Q2:class 模块和选择器里写的 .box 冲突吗?

A:不冲突,两者会合并生效。选择器里的类适合写"固定不变的类",class 模块适合控制"会动态切换的类"。

Q3:on 能绑定自定义事件吗?

A:可以。on 底层就是 addEventListener,原生事件和自定义事件都支持。

Q4:能给同一个事件绑多个处理函数吗?

A:可以,写成数组:on: { click: [fn1, fn2] },触发时按顺序执行。


🧪 小练习

渲染一个按钮,要求:初始文字为"关注",点击后文字变成"已关注",并加上 active 类名和灰色背景。

main.js
import { init, h, classModule, styleModule, eventListenersModule } from 'snabbdom';

const patch = init([classModule, styleModule, eventListenersModule]);

// 请在这里编写代码(提示:点击时创建新 vnode 并调用 patch)

🎉 恭喜你已经掌握了 Snabbdom 的模块机制!下一篇我们深入 patch 函数,看看渲染与更新的完整流程。