跳到主要内容

Vue 2 与 Vue 3 指令的区别

“Vue 2 和 Vue 3 指令有什么区别”这个题,很多人会直接答成“v-model 变了”。这没错,但不完整。

更严谨地说,这道题至少有三层:

  1. 内置指令语义差异
  2. 自定义指令生命周期差异
  3. 模板编译规则变化对指令行为的影响

面试速答(30 秒版 TL;DR)

  • 最常考的变化有四个:
    • 组件上的 v-model 协议变了:value/input -> modelValue/update:modelValue
    • .sync 被移除,统一收敛到 v-model:arg
    • 自定义指令钩子名改成更接近组件生命周期:bind -> beforeMountinserted -> mounted
    • v-ifv-for 的优先级、keytemplate v-for 上的放置方式等编译细节有变化

先按三类差异记:内置指令自定义指令编译规则

1. 最大变化:组件上的 v-model

Vue 2:

<Child v-model="title" />

默认等价于:

<Child :value="title" @input="title = $event" />

Vue 3:

<Child v-model="title" />

默认等价于:

<Child :modelValue="title" @update:modelValue="title = $event" />

同时 Vue 3 支持多个 v-model

<UserForm v-model:name="name" v-model:age="age" />

这在 Vue 2 里通常要靠自定义 prop + event 或 .sync 曲线实现。

2. .sync 被移除

Vue 2 中常见:

<Dialog :visible.sync="visible" />

Vue 3 推荐统一写成:

<Dialog v-model:visible="visible" />

为什么移除?

因为 Vue 3 想把“父子双向同步”的语义统一收敛到 v-model,避免:

  • 一部分逻辑走 v-model
  • 一部分逻辑走 .sync

造成心智模型分裂。

3. 自定义指令生命周期变化

Vue 2 自定义指令钩子:

  • bind
  • inserted
  • update
  • componentUpdated
  • unbind

Vue 3 改成:

  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted

对照关系如下:

Vue 2Vue 3
bindbeforeMount
insertedmounted
updatebeforeUpdate
componentUpdatedupdated
unbindunmounted

为什么这么改?

因为 Vue 3 统一了组件和指令的生命周期命名体系,理解成本更低,也更一致。

4. 自定义指令上下文对象也有变化

Vue 2 里你可能会用:

  • vnode.context
  • binding.expression

Vue 3 里常见变化是:

  • binding.expression 被移除
  • 组件实例通过 binding.instance 获取

所以老指令迁移时,不能只改钩子名,很多上下文字段也要一起检查。

5. v-ifv-for 的优先级变化

这点很容易被忽略,但面试挺爱问。

  • Vue 2v-for 优先级高于 v-if
  • Vue 3v-if 优先级高于 v-for

这意味着:

<li v-for="item in list" v-if="item.visible" :key="item.id">
{{ item.name }}
</li>

在两代里的编译结果和变量可见性理解会不一样。

实践建议

不要把 v-ifv-for 写在同一个元素上,推荐改写为:

<template v-for="item in visibleList" :key="item.id">
<li>{{ item.name }}</li>
</template>

或者在计算属性里先过滤再渲染。

6. template v-forkey 放置方式变化

Vue 2 中,很多场景会把 key 写在 template 内部的真实元素上。

Vue 3 更强调:

<template v-for="item in list" :key="item.id">
<li>{{ item.name }}</li>
</template>

也就是把 key 放在 template v-for 本身上,语义更清晰。

7. 自定义指令在多根节点组件上的边界

Vue 3 支持 Fragment,多根节点组件变常见了。这会带来一个指令边界:

  • 如果把自定义指令直接用在组件上
  • 但该组件不是单根节点

那指令可能无法像 Vue 2 那样自然落到唯一根元素上。

这不是“指令失效”,而是组件不再保证只有一个真实根节点。实践里更稳的方式是:

  • 把指令直接挂在真实 DOM 元素上
  • 不要依赖“组件一定有唯一根元素”的旧假设

8. 常见迁移误区

8.1 只改 v-model 名字,不改事件名

错误迁移:

<script setup>
defineProps({
modelValue: String,
})
</script>

但还是触发:

emit('input', value)

这样父组件不会更新。Vue 3 必须配合 update:modelValue

8.2 自定义指令只改钩子名,不检查 binding 字段

老项目里这类坑很多。因为有些旧字段在 Vue 3 里已经不存在或语义变了。

8.3 还把 v-ifv-for 叠在一起写

这不只是“风格问题”,而是可读性和行为一致性问题。

9. 面试高频答法

Q1:Vue 2 和 Vue 3 的 v-model 差异是什么?

:组件上默认协议从 value + input 变成了 modelValue + update:modelValue,并且 Vue 3 支持多个 v-model.sync 也被统一收敛到了 v-model:arg

Q2:自定义指令迁移时最容易漏什么?

:最容易漏两件事,一是只改钩子名不改上下文字段,二是忽略 Vue 3 的多根节点能力,仍然默认组件有唯一根元素。

速记要点

  • v-modelvalue/input -> modelValue/update:modelValue
  • .sync:移除,统一到 v-model:arg
  • 指令钩子:改成组件生命周期风格
  • v-if / v-for:优先级变化
  • template v-forkey 放在 template 上更符合 Vue 3 语义