Skip to content

第 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 的指令分为两大类:

  1. 编译时指令v-ifv-elsev-forv-slot——它们在编译阶段被转换为完全不同的代码结构,运行时不存在"指令"的概念
  2. 运行时指令v-modelv-showv-onv-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: 0key: 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
}

withDirectives 不做任何 DOM 操作——它只是将指令信息挂到 VNode 的 dirs 属性上。真正的操作在 patch 阶段触发。

指令钩子的调用时机

typescript
// packages/runtime-core/src/directives.ts
export function invokeDirectiveHook(
  vnode: VNode,
  prevVNode: VNode | null,
  instance: ComponentInternalInstance | null,
  name: keyof ObjectDirective
) {
  const bindings = vnode.dirs!
  const oldBindings = prevVNode && prevVNode.dirs!

  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i]
    if (oldBindings) {
      binding.oldValue = oldBindings[i].value
    }
    let hook = binding.dir[name] as DirectiveHook | DirectiveHook[] | undefined

    if (hook) {
      pauseTracking()
      callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
        vnode.el,      // 真实 DOM 元素
        binding,       // 指令绑定对象
        vnode,         // 当前 VNode
        prevVNode      // 旧 VNode
      ])
      resetTracking()
    }
  }
}

mountElementpatchElement 中调用:

typescript
// packages/runtime-core/src/renderer.ts(简化)

// 挂载元素时
const mountElement = (vnode, container, anchor, ...) => {
  // 创建 DOM
  el = vnode.el = hostCreateElement(vnode.type)

  // 设置属性
  if (props) { /* ... */ }

  // 指令 created 钩子
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'created')
  }

  // 挂载子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(vnode.children, el, null, ...)
  }

  // 指令 beforeMount 钩子
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  }

  // 插入 DOM
  hostInsert(el, container, anchor)

  // 指令 mounted 钩子(Post 队列)
  if (dirs) {
    queuePostRenderEffect(() => {
      invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

// 更新元素时
const patchElement = (n1, n2, ...) => {
  const el = (n2.el = n1.el!)

  const { dirs } = n2

  // 指令 beforeUpdate 钩子
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }

  // 更新属性和子节点 ...

  // 指令 updated 钩子(Post 队列)
  if (dirs) {
    queuePostRenderEffect(() => {
      invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

指令的生命周期与元素的生命周期完美对齐:

13.3 v-model 的实现

v-model 是 Vue 中最复杂的指令——它需要根据不同的表单元素类型(input、textarea、select、checkbox、radio)采用不同的实现策略。

基于 VitePress 构建