模块机制:样式、属性与事件
🎯 引言
上一篇创建的虚拟节点只描述了页面结构,还不能加样式、不能绑事件。这篇我们学习 Snabbdom 的模块机制,掌握六大常用模块,让虚拟节点拥有完整的页面表现力。
🧱 为什么需要模块
Snabbdom 的核心极其精简,只认识 sel、text、children、key 这四样东西。那 class、样式、事件怎么办?答案是:全部做成模块,用哪个装哪个。
这就像买手机:核心是一台裸机,拍照、游戏等能力按需安装 App。模块通过 init 注册:
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 模块用对象控制类名,键是类名,值是布尔值:
const vnode = h(
'div.card',
{
class: { active: true, hidden: false }
},
'卡片'
);
渲染结果:
<div class="card active">卡片</div>
更新时把 active 改成 false,Snabbdom 会精准地移除这个类,其他类不受影响。
style 模块用对象写行内样式,属性名用驼峰写法:
const vnode = h(
'div',
{
style: { color: 'red', fontSize: '20px' }
},
'红色文字'
);
渲染结果:
<div style="color: red; font-size: 20px;">红色文字</div>
delayed(元素插入后延迟应用的样式),配合 CSS transition 可以实现入场动画,属于进阶用法,了解即可。⚖️ props 和 attrs 的区别(重点)
这两个模块最容易混淆,必须分清:
| 模块 | data 字段 | 操作方式 | 体现在 HTML 标签上? |
|---|---|---|---|
| propsModule | props | 直接设置 DOM 对象的属性 | 不一定 |
| attributesModule | attrs | 调用 setAttribute 设置 HTML 特性 | 是 |
看一组对比实验:
// props:设置 DOM 属性
h('input', { props: { value: 'hello' } });
// 渲染结果:<input>,标签上看不到 value,但输入框里确实有值
// attrs:设置 HTML 特性
h('input', { attrs: { value: 'hello', placeholder: '请输入' } });
// 渲染结果:<input value="hello" placeholder="请输入">
选型建议:
- 操作表单值(
value、checked)、disabled这类 DOM 状态,用props。 - 操作
placeholder、title、data-*这类标签特性,用attrs。
📦 dataset 模块与事件模块
dataset 模块专门设置 data-* 自定义特性,键名会自动转成短横线风格:
h(
'div',
{
dataset: { action: 'reset', userId: 100 }
},
'内容'
);
// 渲染结果:<div data-action="reset" data-user-id="100">内容</div>
事件模块用 on 字段绑定事件:
const vnode = h(
'button',
{
on: {
click: () => alert('被点击了')
}
},
'点我'
);
最省心的是:更新或移除节点时,旧的事件监听器会被 Snabbdom 自动移除,不需要手动解绑,从根源上避免了内存泄漏。
🛠 综合示例
把几个模块组合起来,渲染一张带交互的卡片:
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 类名和灰色背景。
import { init, h, classModule, styleModule, eventListenersModule } from 'snabbdom';
const patch = init([classModule, styleModule, eventListenersModule]);
// 请在这里编写代码(提示:点击时创建新 vnode 并调用 patch)
🎉 恭喜你已经掌握了 Snabbdom 的模块机制!下一篇我们深入 patch 函数,看看渲染与更新的完整流程。
