2026.01.03
patchProp -> Vue 的属性更新机制
在前面的章节中,我们已经实现了基础的渲染器。现在让我们为虚拟节点添加属性支持:
patchProp 的作用
在前面的章节中,我们已经实现了基础的渲染器。现在让我们为虚拟节点添加属性支持:
import { createRenderer, h } from 'vue'
const vnode = h('div', { class: 'container' }, 'hello world')
const renderer = createRenderer(renderOptions)
renderer.render(vnode, app)
运行上面的代码会遇到错误:

错误提示我们缺少 patchProp 方法。这是因为 createRenderer 需要一个处理属性的方法。
实现 patchProp
在 nodeOps.ts 同级创建 patchProp.ts,并修改导出:
// runtime-dom/src/index.ts
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
export * from '@vue/runtime-core'
// 合并 DOM 操作方法和属性更新方法
const renderOptions = { patchProp, ...nodeOps }
export { renderOptions }
实现 patchClass
class 是最常用的属性之一,我们首先实现对它的支持:
// patchProp.ts
export function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
patchClass(el, nextValue)
}
}
function patchClass(el, value) {
if (value) {
el.className = value
}
}
这样可以在初始化时设置 class,但如果要更新或移除 class 呢?
import { createRenderer, h } from 'vue'
const vnode = h('div', { class: 'container' }, 'hello world')
const renderer = createRenderer(renderOptions)
renderer.render(vnode, app)
// 1 秒后移除 class
const vnode2 = h('div', {}, 'hello world')
setTimeout(() => {
renderer.render(vnode2, app)
}, 1000)
上面的代码中,vnode2 没有 class 属性,所以我们需要将其移除:
function patchClass(el, nextValue) {
if (nextValue) {
el.className = nextValue
}
else {
// 如果新值为空,移除 class 属性
el.removeAttribute('class')
}
}
实现 patchStyle
样式处理比 class 更复杂,因为需要对比新旧样式对象:
function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
return patchClass(el, nextValue)
}
if (key === 'style') {
return patchStyle(el, prevValue, nextValue)
}
}
function patchStyle(el, prevValue, nextValue) {
const style = el.style
if (nextValue) {
// 设置新样式
// 例如:h('div', { style: { color: 'red' } }, 'hello world')
for (const key in nextValue) {
style[key] = nextValue[key]
}
}
if (prevValue) {
// 移除旧样式中存在但新样式中不存在的属性
// 例如:prevValue = { background: 'pink', color: 'yellow' }
// nextValue = { color: 'red' }
// 需要移除 background
for (const key in prevValue) {
if (!(key in nextValue)) {
style[key] = null
}
}
}
}
实现 patchEvent
事件处理是 Vue 中非常重要的特性。让我们来实现事件的绑定和更新:
const renderer = createRenderer(renderOptions)
const vnode = h('div', {
onClick() {
console.log('click')
}
}, 'hello world')
renderer.render(vnode, app)
此时传递的 key 是 onClick,我们需要判断是否以 on 开头来识别事件:
function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
return patchClass(el, nextValue)
}
if (key === 'style') {
return patchStyle(el, prevValue, nextValue)
}
if (/^on[A-Z]/.test(key)) {
return patchEvent(el, key, prevValue, nextValue)
}
}
基础实现
最简单的实现方式:
function patchEvent(el, rawName, prevValue, nextValue) {
// 提取事件名:onClick -> click
const name = rawName.slice(2).toLowerCase()
el.addEventListener(name, nextValue)
}
这样点击事件可以执行了,但如果遇到更新的情况会有问题:
const renderer = createRenderer(renderOptions)
const vnode = h('div', {
onClick() {
console.log('click')
}
}, 'hello world')
const vnode2 = h('div', {
onClick() {
console.log('click2')
}
}, 'hello world')
renderer.render(vnode, app)
setTimeout(() => {
renderer.render(vnode2, app)
}, 1000)
这样会导致点击时同时触发两个事件处理函数。我们需要改进:
改进:先移除再添加
function patchEvent(el, rawName, prevValue, nextValue) {
const name = rawName.slice(2).toLowerCase()
// 如果有旧的事件处理函数,先移除
if (prevValue) {
el.removeEventListener(name, prevValue)
}
// 添加新的事件处理函数
el.addEventListener(name, nextValue)
}
这样虽然解决了问题,但每次更新都要先移除再添加,性能不够好。
优化:使用事件调用器(Invoker)
更优的方案是使用一个调用器函数,更新时只需要替换调用器内部的函数引用:
function createInvoker(value) {
// 创建一个事件处理函数,内部调用 invoker.value
// 当需要更新事件时,只需更新 invoker.value 即可
const invoker = (e) => {
invoker.value(e)
}
invoker.value = value
return invoker
}
// 使用 Symbol 作为存储事件调用器的 key
const veiKey = Symbol('_vei')
export function patchEvent(el, rawName, nextValue) {
const name = rawName.slice(2).toLowerCase()
// 在元素上存储所有事件调用器:el._vei = {}
const invokers = (el[veiKey] ??= {})
// 获取之前绑定的 invoker
const existingInvoker = invokers[rawName]
if (nextValue) {
if (existingInvoker) {
// 如果已经存在 invoker,只需更新其 value 属性
existingInvoker.value = nextValue
return
}
// 创建新的 invoker 并绑定
const invoker = createInvoker(nextValue)
invokers[rawName] = invoker
el.addEventListener(name, invoker)
}
else {
// 如果没有新的事件处理函数,但旧的存在,则移除
if (existingInvoker) {
el.removeEventListener(name, existingInvoker)
invokers[rawName] = undefined
}
}
}
这种方案的优势:
- 性能更好:更新事件时不需要移除和重新绑定监听器
- 内存友好:避免了频繁创建和销毁监听器
- 更符合 Vue 的设计理念:通过间接引用实现灵活的更新机制
实现 patchAttr
对于其他普通属性(如 id、data-* 等),我们需要一个通用的处理方法:
export function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
return patchClass(el, nextValue)
}
if (key === 'style') {
return patchStyle(el, prevValue, nextValue)
}
if (isOn(key)) {
return patchEvent(el, key, nextValue)
}
patchAttr(el, key, nextValue)
}
function patchAttr(el, key, value) {
if (value == null) {
// 值为 null 或 undefined 时,移除属性
el.removeAttribute(key)
}
else {
// 设置属性
el.setAttribute(key, value)
}
}
代码结构优化
随着功能增多,我们需要将 patchProp 相关代码进行模块化拆分:
runtime-dom/
├── package.json
└── src
├── index.ts
├── modules
│ ├── patchAttr.ts
│ ├── patchClass.ts
│ ├── patchEvent.ts
│ └── patchStyle.ts
├── nodeOps.ts
└── patchProp.ts
整合后的 patchProp.ts
import { isOn } from '@vue/shared'
import { patchAttr } from './modules/patchAttr'
import { patchClass } from './modules/patchClass'
import { patchEvent } from './modules/patchEvent'
import { patchStyle } from './modules/patchStyle'
export function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
return patchClass(el, nextValue)
}
if (key === 'style') {
return patchStyle(el, prevValue, nextValue)
}
if (isOn(key)) {
return patchEvent(el, key, nextValue)
}
patchAttr(el, key, nextValue)
}
总结
通过实现 patchProp 及其相关方法,我们为渲染器添加了完整的属性更新能力:
- patchClass:高效处理 class 的添加、更新和移除
- patchStyle:支持样式对象的 diff 更新
- patchEvent:通过 invoker 模式优化事件绑定性能
- patchAttr:处理通用 HTML 属性
这些方法共同构成了 Vue 高效的属性更新机制,是虚拟 DOM diff 算法的重要组成部分。