patch 函数:渲染与更新
🎯 引言
h 负责"画图纸",真正"施工"的是 patch。这篇我们弄清 patch 的两种用法、首次渲染时容器被替换的机制,以及一个几乎所有新手都会踩的坑。
🧱 patch 的两种用法
patch 接收两个参数,根据第一个参数的不同,行为完全不同:
// 用法一:patch(真实DOM元素, vnode) —— 首次渲染
patch(document.getElementById('app'), vnode);
// 用法二:patch(oldVnode, newVnode) —— 更新
patch(vnode, newVnode);
| 场景 | 第一个参数 | 第二个参数 | 行为 |
|---|---|---|---|
| 首次渲染 | 真实 DOM 元素(容器) | 虚拟节点 | 创建真实元素并挂载 |
| 更新 | 旧的虚拟节点 | 新的虚拟节点 | 对比差异,最小化更新 |
⚠️ 注意:首次渲染会替换容器
首次渲染时有个重要细节:patch 会用 vnode 生成的元素替换掉容器本身,而不是塞进容器里面。
// HTML 里原本有一个 <div id="app"></div>
const vnode = h('p', '第一段');
patch(document.getElementById('app'), vnode);
渲染后页面结构是:
<p>第一段</p>
原来的 <div id="app"> 已经不在页面上了!所以下面的写法是错误的:
// ❌ 错误示范:第二次还通过 getElementById 拿容器去 patch
patch(document.getElementById('app'), newVnode);
此时 document.getElementById('app') 拿到的是 null,直接报错。
正确姿势:永远保存上一次 patch 返回的 vnode,用它来做下一次更新:
// ✅ 正确姿势
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 都一致,第六篇细讲):
情况一:节点相同 → 原地精细更新。 复用真实元素,只改有差异的部分:
let vnode = h('div', '更新前');
vnode = patch(document.getElementById('app'), vnode);
// sel 相同,真实 div 被复用,只更新文字
vnode = patch(vnode, h('div', '更新后'));
情况二:节点不同 → 直接替换。 销毁旧元素,创建新元素换上:
// 从 div 变成 p,sel 不同,整个元素被替换
vnode = patch(vnode, h('p', '我是新来的'));
记住这条规律:尽可能保持 sel 稳定,让 Snabbdom 走"原地更新"路径,性能最好。
🪝 钩子函数简介
上一篇说过,模块通过钩子函数介入渲染过程。Snabbdom 提供了完整的生命周期钩子,常用的有:
| 钩子 | 触发时机 |
|---|---|
create | 真实元素被创建后 |
insert | 元素被插入页面后 |
update | 节点更新时 |
destroy | 元素被移除后 |
以 insert 为例,实现"输入框渲染后自动聚焦":
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 面板中观察变化过程。
import { init, h } from 'snabbdom';
const patch = init([]);
// 请在这里编写代码(提示:用 setTimeout 定时 patch)
🎉 恭喜你已经掌握了 patch 的完整流程!下一篇我们学习列表渲染中至关重要的 key。
