主题
实现渲染器 - update 流程
本节对标 Vue 3 源码
@vue/runtime-core中的renderer.ts—— update / patch 部分
update 的触发时机
当响应式数据变化时,组件的 render effect 重新执行,生成新的 VNode 树。渲染器对比新旧 VNode 树,只更新变化的部分。
数据变化 → trigger → 组件 render effect → 新 VNode → patch(oldVNode, newVNode) → DOM 更新patchElement —— 更新 DOM 元素
ts
// patchElement 负责更新已有的 DOM 元素节点
function patchElement(n1: VNode, n2: VNode) {
// 复用旧 VNode 的真实 DOM 引用,同时赋给新 VNode
// 这是 Diff 的核心思想:相同类型的节点复用 DOM,只更新差异部分
const el = (n2.el = n1.el!)
const oldProps = n1.props || {} // 旧属性,不存在时用空对象兜底
const newProps = n2.props || {} // 新属性
// 第 1 步:对比并更新属性(class、style、事件等)
patchProps(el, oldProps, newProps)
// 第 2 步:递归更新子节点(文本 / 数组 / 空的组合)
patchChildren(n1, n2, el)
}update 的核心就两步:更新属性 + 更新子节点。
patchProps —— 属性更新
ts
// patchProps 对比新旧属性,执行最小化的属性更新操作
function patchProps(
el: any,
oldProps: Record<string, any>,
newProps: Record<string, any>,
) {
// 引用相同说明属性完全没变,直接跳过
if (oldProps === newProps) return
// 遍历新属性:如果值与旧值不同,则更新(覆盖旧值或添加新属性)
for (const key in newProps) {
const prev = oldProps[key]
const next = newProps[key]
if (prev !== next) {
patchProp(el, key, prev, next)
}
}
// 遍历旧属性:如果某个 key 在新属性中不存在,则将其删除(传 null 表示删除)
for (const key in oldProps) {
if (!(key in newProps)) {
patchProp(el, key, oldProps[key], null)
}
}
}patchProp 的实现(runtime-dom)
ts
// patchProp 是 runtime-dom 提供的属性设置函数
// 根据属性名的特征分发到不同的处理逻辑
export function patchProp(
el: Element,
key: string,
prevValue: any,
nextValue: any,
) {
if (key === 'class') {
// class 属性走专门的优化路径
patchClass(el, nextValue)
} else if (key === 'style') {
// style 属性需要逐个 CSS 属性对比
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// 以 on 开头的属性视为事件监听器(如 onClick、onInput)
patchEvent(el, key, prevValue, nextValue)
} else {
// 其他普通 HTML 属性(如 id、href、disabled 等)
patchAttr(el, key, nextValue)
}
}
// 判断属性名是否为事件监听器:以 "on" 开头且第三个字符是大写字母
function isOn(key: string): boolean {
return /^on[A-Z]/.test(key)
}patchClass
ts
// patchClass 使用 className 直接赋值来设置 class
// 比 setAttribute('class', value) 性能更好(浏览器内部优化)
function patchClass(el: Element, value: string | null) {
if (value == null) {
el.removeAttribute('class') // null 或 undefined 时移除 class 属性
} else {
el.className = value // 直接赋值,性能最优
}
}使用 className 而非 setAttribute('class', value),性能更好。
patchStyle
ts
// patchStyle 对比新旧 style 对象,精确更新每个 CSS 属性
function patchStyle(
el: HTMLElement,
prev: Record<string, string> | null,
next: Record<string, string> | null,
) {
const style = el.style
if (!next) {
// 新样式为空 → 直接移除整个 style 属性
el.removeAttribute('style')
return
}
// 遍历新样式对象,逐一设置 CSS 属性
for (const key in next) {
style.setProperty(key, next[key])
}
// 清理:遍历旧样式,移除新样式中已不存在的 CSS 属性
if (prev) {
for (const key in prev) {
if (!(key in next)) {
style.removeProperty(key)
}
}
}
}patchEvent
ts
// patchEvent 使用 invoker 模式管理事件监听
// 核心优化:更新事件时只替换 invoker.value,不需要反复 removeEventListener + addEventListener
function patchEvent(
el: Element & { _vei?: Record<string, any> }, // _vei = vue event invokers 的缩写
rawName: string, // 原始事件名,如 "onClick"
prevValue: any, // 旧的事件处理函数
nextValue: any, // 新的事件处理函数
) {
// 获取或初始化元素上的 invoker 缓存对象
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName] // 查找此事件名是否已有 invoker
const name = rawName.slice(2).toLowerCase() // "onClick" → "click"
if (nextValue && existingInvoker) {
// 情况 1:事件已存在且有新值 → 只需替换 invoker 内部的 value(高效更新)
existingInvoker.value = nextValue
} else if (nextValue) {
// 情况 2:新增事件 → 创建 invoker 并注册到 DOM
const invoker = (invokers[rawName] = createInvoker(nextValue))
el.addEventListener(name, invoker)
} else if (existingInvoker) {
// 情况 3:删除事件 → 移除监听并清除 invoker 缓存
el.removeEventListener(name, existingInvoker)
invokers[rawName] = undefined
}
}
// 创建 invoker 包装函数:真正的事件处理函数存储在 invoker.value 上
// 执行时调用 invoker.value(e),这样更新处理函数时只需替换 value 即可
function createInvoker(initialValue: any) {
const invoker = (e: Event) => {
invoker.value(e) // 通过 value 间接调用真正的处理函数
}
invoker.value = initialValue // 初始化 value 为传入的处理函数
return invoker
}关键设计:事件使用 invoker 模式。更新事件处理函数时,只需替换 invoker.value,不需要 removeEventListener + addEventListener,性能更优。
patchChildren —— 子节点更新
子节点更新是渲染器最复杂的部分。新旧子节点有 3 种类型(文本、数组、空),组合出 9 种情况:
ts
// patchChildren 处理子节点的更新
// 新旧子节点各有 3 种类型(文本、数组、空),组合出 9 种情况
function patchChildren(n1: VNode, n2: VNode, container: any) {
const c1 = n1.children // 旧子节点
const c2 = n2.children // 新子节点
const prevShapeFlag = n1.shapeFlag // 旧 VNode 的类型标志
const shapeFlag = n2.shapeFlag // 新 VNode 的类型标志
// ===== 情况一:新子节点是文本 =====
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧的是数组 → 需要先卸载所有旧子节点
unmountChildren(c1 as VNode[])
}
// 文本内容不同时才更新(包括旧的是文本或空的情况)
if (c1 !== c2) {
setElementText(container, c2 as string)
}
return
}
// ===== 情况二:新子节点是数组 =====
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新旧都是数组 → 这是最复杂的情况,需要 Diff 算法精确更新
patchKeyedChildren(c1 as VNode[], c2 as VNode[], container)
} else {
// 旧的是文本或空 → 清空文本后挂载新的子节点数组
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
setElementText(container, '')
}
mountChildren(c2 as VNode[], container)
}
return
}
// ===== 情况三:新子节点为空 =====
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧的是数组 → 卸载所有旧子节点
unmountChildren(c1 as VNode[])
} else if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 旧的是文本 → 清空文本内容
setElementText(container, '')
}
// 旧的也是空 → 不需要任何操作
}9 种情况整理
| 旧 \ 新 | 文本 | 数组 | 空 |
|---|---|---|---|
| 文本 | 替换文本 | 清空文本 + 挂载 | 清空文本 |
| 数组 | 卸载 + 设置文本 | Diff | 卸载 |
| 空 | 设置文本 | 挂载 | 无操作 |
其中数组 → 数组是最复杂的情况,需要 Diff 算法处理。
unmountChildren
ts
// unmountChildren 批量卸载子节点数组
function unmountChildren(children: VNode[]) {
for (let i = 0; i < children.length; i++) {
unmount(children[i]) // 逐个调用 unmount 移除真实 DOM
}
}完整的更新流程
响应式数据变化
│
▼
trigger → effect.scheduler → queueJob
│
▼
nextTick(微任务)
│
▼
组件 render() 生成新 VNode
│
▼
patch(oldVNode, newVNode, container)
│
├── 类型不同? → unmount(old) + mount(new)
│
└── 类型相同? → patchElement / patchComponent
│
├── patchProps(属性对比更新)
│
└── patchChildren(子节点对比更新)
│
└── 都是数组? → Diff 算法测试用例
ts
describe('renderer: update', () => {
// 测试元素属性的更新和删除
it('should update element props', () => {
const root = document.createElement('div')
// 首次渲染:id="foo" class="bar"
render(h('div', { id: 'foo', class: 'bar' }), root)
expect(root.innerHTML).toBe('<div id="foo" class="bar"></div>')
// 更新渲染:id 变为 "baz",class 被删除(新 props 中没有 class)
render(h('div', { id: 'baz' }), root)
expect(root.innerHTML).toBe('<div id="baz"></div>')
})
// 测试文本子节点的更新
it('should update text children', () => {
const root = document.createElement('div')
render(h('div', null, 'old'), root)
expect(root.innerHTML).toBe('<div>old</div>')
// 文本从 'old' 变为 'new',应该只更新文本内容
render(h('div', null, 'new'), root)
expect(root.innerHTML).toBe('<div>new</div>')
})
// 测试子节点类型从文本切换到数组
it('should update from text to array children', () => {
const root = document.createElement('div')
render(h('div', null, 'text'), root) // 初始为文本子节点
// 更新为数组子节点:先清空文本,再挂载子元素
render(h('div', null, [h('span', null, 'child')]), root)
expect(root.innerHTML).toBe('<div><span>child</span></div>')
})
// 测试 render(null) 时的卸载行为
it('should unmount when render null', () => {
const root = document.createElement('div')
render(h('div', null, 'hello'), root)
expect(root.innerHTML).toBe('<div>hello</div>')
// 传入 null 应触发 unmount,清空容器内容
render(null, root)
expect(root.innerHTML).toBe('')
})
})本节小结
- patchElement — 更新属性 + 更新子节点
- patchProps — 遍历新旧属性,添加/更新/删除
- patchEvent — invoker 模式,避免频繁的 addEventListener/removeEventListener
- patchChildren — 9 种子节点组合情况,核心是数组 → 数组的 Diff
- 最小化 DOM 操作 — 只更新真正变化的部分
下一节实现 Diff 算法 —— 渲染器中最核心的优化策略。