Appearance
第 13 章 指令系统
本章要点
- 指令的本质:将 DOM 操作逻辑封装为可复用的声明式抽象
- 内置指令的实现:v-model、v-show、v-if、v-for、v-on、v-bind 的编译与运行时协作
- 自定义指令的完整生命周期:created → beforeMount → mounted → beforeUpdate → updated → beforeUnmount → unmounted
- 指令的编译时转换:编译器如何将指令语法转换为 withDirectives 调用
- withDirectives 的运行时机制:指令绑定的创建、更新与销毁
- v-model 的双向绑定在不同元素类型上的差异化实现
- 指令与组件的交互:组件上使用指令的限制与解决方案
指令是 Vue 模板语法中最具"魔法感"的部分。当你写下 v-model="name" 时,输入框自动与变量双向绑定;写下 v-show="visible" 时,元素的显隐自动切换。这些"魔法"的背后是编译器和运行时的精密协作。
在前面的章节中,我们已经了解了编译器如何处理模板、运行时如何创建和更新 VNode。本章将聚焦于连接这两者的关键机制——指令系统。
13.1 指令的分类与编译
编译时指令 vs 运行时指令
Vue 的指令分为两大类:
- 编译时指令:
v-if、v-else、v-for、v-slot——它们在编译阶段被转换为完全不同的代码结构,运行时不存在"指令"的概念 - 运行时指令:
v-model、v-show、v-on、v-bind、自定义指令——它们在运行时通过withDirectives注册生命周期钩子
v-if 的编译转换
v-if 在编译阶段被完全消解:
html
<template>
<div v-if="show">Hello</div>
<div v-else>Bye</div>
</template>编译为:
typescript
function render(_ctx) {
return _ctx.show
? (_openBlock(), _createElementBlock("div", { key: 0 }, "Hello"))
: (_openBlock(), _createElementBlock("div", { key: 1 }, "Bye"))
}变成了一个简单的三元表达式。注意 key: 0 和 key: 1——编译器自动为不同的分支添加不同的 key,确保 Diff 算法能正确识别它们是不同的节点。
v-for 的编译转换
html
<template>
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</template>编译为:
typescript
function render(_ctx) {
return (_openBlock(true), _createElementBlock(
_Fragment, null,
_renderList(_ctx.items, (item) => {
return (_openBlock(), _createElementBlock("div", { key: item.id },
_toDisplayString(item.name), 1 /* TEXT */))
}),
128 /* KEYED_FRAGMENT */
))
}v-for 被转换为 _renderList 调用(本质是 Array.map),每个迭代项生成一个独立的 Block。外层包裹一个 Fragment,并标记 KEYED_FRAGMENT PatchFlag。
openBlock(true) 中的 true 参数表示禁用 Block 追踪——因为 v-for 的子节点数量是动态的,不能用固定的 dynamicChildren 来优化,必须走完整的 keyed Diff。
13.2 withDirectives:运行时指令的核心
运行时指令通过 withDirectives 函数附加到 VNode 上:
typescript
// 编译器输出示例
_withDirectives(
_createElementVNode("input", {
"onUpdate:modelValue": $event => (_ctx.name = $event)
}, null, 8, ["onUpdate:modelValue"]),
[
[_vModelText, _ctx.name]
]
)withDirectives 的实现:
typescript
// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>(
vnode: T,
directives: DirectiveArguments
): T {
if (currentRenderingInstance === null) {
warn(`withDirectives can only be used inside render functions.`)
return vnode
}
const instance = getExposeProxy(currentRenderingInstance) || currentRenderingInstance.proxy
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
// 函数简写:将函数同时注册为 mounted 和 updated 钩子
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir
} as ObjectDirective
}
// 如果指令有 deep 选项,创建深度响应式 watch
if (dir.deep) {
traverse(value)
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}