Snabbdom 简介

patch 函数:渲染与更新

掌握 patch 的两种用法,理解首次渲染替换容器的机制和更新时的两条路径,避开重复用容器 patch 的坑。

🎯 引言

h 负责"画图纸",真正"施工"的是 patch。这篇我们弄清 patch 的两种用法、首次渲染时容器被替换的机制,以及一个几乎所有新手都会踩的坑。


🧱 patch 的两种用法

patch 接收两个参数,根据第一个参数的不同,行为完全不同:

main.js
// 用法一:patch(真实DOM元素, vnode) —— 首次渲染
patch(document.getElementById('app'), vnode);

// 用法二:patch(oldVnode, newVnode) —— 更新
patch(vnode, newVnode);
场景第一个参数第二个参数行为
首次渲染真实 DOM 元素(容器)虚拟节点创建真实元素并挂载
更新旧的虚拟节点新的虚拟节点对比差异,最小化更新

⚠️ 注意:首次渲染会替换容器

首次渲染时有个重要细节:patch 会用 vnode 生成的元素替换掉容器本身,而不是塞进容器里面。

main.js
// HTML 里原本有一个 <div id="app"></div>
const vnode = h('p', '第一段');
patch(document.getElementById('app'), vnode);

渲染后页面结构是:

<p>第一段</p>

原来的 <div id="app"> 已经不在页面上了!所以下面的写法是错误的:

main.js
// ❌ 错误示范:第二次还通过 getElementById 拿容器去 patch
patch(document.getElementById('app'), newVnode);

此时 document.getElementById('app') 拿到的是 null,直接报错。

就算你把容器元素提前存进变量反复用,也一样行不通:容器已被替换出页面,对它 patch 不会产生任何页面变化(静默失败,很难排查)。

正确姿势:永远保存上一次 patch 返回的 vnode,用它来做下一次更新

main.js
// ✅ 正确姿势
let vnode = h('p', '第一段');
vnode = patch(document.getElementById('app'), vnode); // 保存返回的 vnode

const newVnode = h('p', '第二段');
vnode = patch(vnode, newVnode); // 用旧 vnode 更新

patch 的返回值就是新的虚拟节点。把它存下来,就形成了"旧 vnode → patch → 新 vnode → 再 patch"的更新循环。


⚙️ 更新时的两条路径

执行 patch(oldVnode, newVnode) 时,Snabbdom 会先判断两个节点是否"相同"(选择器和 key 都一致,第六篇细讲):

情况一:节点相同 → 原地精细更新。 复用真实元素,只改有差异的部分:

main.js
let vnode = h('div', '更新前');
vnode = patch(document.getElementById('app'), vnode);

// sel 相同,真实 div 被复用,只更新文字
vnode = patch(vnode, h('div', '更新后'));

情况二:节点不同 → 直接替换。 销毁旧元素,创建新元素换上:

main.js
// 从 div 变成 p,sel 不同,整个元素被替换
vnode = patch(vnode, h('p', '我是新来的'));

记住这条规律:尽可能保持 sel 稳定,让 Snabbdom 走"原地更新"路径,性能最好


🪝 钩子函数简介

上一篇说过,模块通过钩子函数介入渲染过程。Snabbdom 提供了完整的生命周期钩子,常用的有:

钩子触发时机
create真实元素被创建后
insert元素被插入页面后
update节点更新时
destroy元素被移除后

insert 为例,实现"输入框渲染后自动聚焦":

main.js
const vnode = h('input', {
    hook: {
        insert: (vn) => vn.elm.focus()
    }
});
patch(document.getElementById('app'), vnode);

钩子的回调能拿到虚拟节点 vn,通过 vn.elm 操作真实元素。日常开发用得不多,但做动画、自动聚焦这类"必须碰到真实 DOM"的需求时非常好用。


🧾 小节总结

  • patch(容器, vnode) 用于首次渲染,patch(oldVnode, newVnode) 用于更新。
  • 首次渲染会用 vnode 生成的元素替换容器本身,之后不能再拿容器去 patch。
  • 正确做法是保存 patch 返回的 vnode,形成"旧 → 新"的更新循环。
  • 更新时:节点相同走原地精细更新,节点不同走整体替换。
  • 钩子函数(如 insert)可以在渲染过程中操作真实 DOM。

❓ 知识问答

Q1:为什么首次渲染要替换容器,而不是往容器里塞?

A:这样 vnode 与真实元素能保持严格的一一对应,后续更新逻辑更简单统一。Vue 挂载时替换 #app 元素,也是同样的设计。

Q2:patch 的返回值可以不接收吗?

A:语法上可以,但更新链条就断了。建议养成 vnode = patch(...) 的习惯。

Q3:连续多次 patch 会有问题吗?

A:不会。每次 patch 都基于上一次的结果做对比,这正是数据驱动界面的工作方式——数据变一次,就创建新 vnode 并 patch 一次。

Q4:怎么把渲染的内容整个清空?

A:可以 patch 一个注释节点:patch(vnode, h('!'))h('!') 会创建一个注释节点,原内容随之被替换掉。


🧪 小练习

实现一个"段落变身"效果:先渲染一个内容为"我是段落"的 p,2 秒后更新为内容"我还是段落"的 p(观察元素复用),再过 2 秒更新为 h2 标题(观察元素替换)。打开浏览器开发者工具,在 Elements 面板中观察变化过程。

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

const patch = init([]);

// 请在这里编写代码(提示:用 setTimeout 定时 patch)

🎉 恭喜你已经掌握了 patch 的完整流程!下一篇我们学习列表渲染中至关重要的 key。