跳到主要内容

Vue 2 / Vue 3 指令的区别:迁移重点与常见坑

很多同学一看到“Vue 2 升 Vue 3 的指令差异”,脑子里会同时冒出一堆问题:

  • v-model 到底变了多少?
  • .sync.native、filters 还在吗?
  • 自定义指令为什么钩子名全改了?
  • v-ifv-for 写在一起,为什么 Vue 3 表现不一样?

这篇文章不打算把所有指令从头背一遍,而是专门讲清楚:Vue 2 和 Vue 3 在“指令相关用法”上,哪些没变,哪些变了,为什么变,以及迁移时该怎么改。

1. 先给结论:真正要重点记的差异只有这几类

类别Vue 2Vue 3影响程度
组件上的 v-model默认是 value + input默认是 modelValue + update:modelValue非常高
多个 v-model不自然,常配合 .sync原生支持 v-model:xxx非常高
自定义指令钩子bind/inserted/update/componentUpdated/unbindcreated/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted
v-ifv-for 同元素优先级v-for 优先v-if 优先
<template v-for>key常写在子元素上应写在 <template>
条件分支上的 key常手动写 key默认自动生成唯一 key;手写时不能复用同 key
v-bind="obj" 合并行为单独属性总是覆盖对象里的同名属性按书写顺序决定谁覆盖谁
v-on.native可用移除
v-on 的 keyCode 修饰符@keyup.13 可用移除,改用按键名
filters可用移除
最省事的记法

Vue 3 不是“指令名字大换血”,而是“少数关键指令的协议和边界变了”。 最重要的其实就三块:v-model、自定义指令、模板优先级/key 规则。

2. 哪些指令其实“基本没变”?

先别自己吓自己。下面这些内置指令在 Vue 2 和 Vue 3 里核心用途基本一致

  • v-if / v-else-if / v-else
  • v-show
  • v-for
  • v-bind
  • v-on
  • v-text
  • v-html
  • v-pre
  • v-cloak
  • v-once

也就是说:

  • 大部分基础模板写法照样能用
  • 真正的破坏性变化,主要集中在“组件双向绑定”“自定义指令 API”“模板边界规则”

3. v-model:Vue 2 和 Vue 3 差异最大的指令

如果只选一个“迁移时最容易出问题的指令”,基本就是 v-model

3.1 原生表单元素上,心智模型几乎没变

无论 Vue 2 还是 Vue 3,下面这些写法的直觉都差不多:

<input v-model="name" />
<textarea v-model="desc" />
<input type="checkbox" v-model="checked" />
<select v-model="city">
<option value="sh">上海</option>
</select>

本质仍然是:

  • 把数据绑定到表单值上
  • 用户输入后再把最新值同步回数据

真正变化大的,不是原生表单,而是组件上的 v-model

3.2 组件上的默认协议变了

Vue 2 中,组件上的 v-model 默认等价于:

  • prop:value
  • event:input
<!-- 父组件 -->
<BaseInput v-model="keyword" />
// Vue 2 子组件
export default {
props: {
value: String,
},
methods: {
onInput(e) {
this.$emit('input', e.target.value)
},
},
}

Vue 3 中,默认协议改成:

  • prop:modelValue
  • event:update:modelValue
<!-- 父组件 -->
<BaseInput v-model="keyword" />
<script>
export default {
props: {
modelValue: String,
},
emits: ['update:modelValue'],
methods: {
onInput(e) {
this.$emit('update:modelValue', e.target.value)
},
},
}
</script>
高频坑

如果你把子组件从 Vue 2 迁到 Vue 3,但内部还在收 value、发 input,父组件的 v-model 往往就“不工作”了。

3.3 Vue 2 的 model 选项,Vue 3 更推荐参数化 v-model

Vue 2 里如果你不想用默认的 value/input,可以通过组件的 model 选项改协议。

Vue 3 的思路更统一:直接用 v-model:字段名

<!-- Vue 3 -->
<UserForm v-model:title="title" v-model:visible="visible" />

子组件对应写法:

<script>
export default {
props: {
title: String,
visible: Boolean,
},
emits: ['update:title', 'update:visible'],
}
</script>

3.4 .sync 基本被 v-model:xxx 取代

Vue 2 常见写法:

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

Vue 3 推荐改成:

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

你可以把它理解成:

  • Vue 2.sync 是“某个 prop 的双向同步补丁”
  • Vue 3:直接并入 v-model 体系,不再额外搞一套语法

3.5 Vue 3 原生支持“多个 v-model

这也是 Vue 3 非常实用的升级点。

<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>

这在 Vue 2 里通常要么拆成多个 prop + 多个事件,要么借助 .sync,语义没有这么统一。

3.6 Vue 3 还支持自定义 v-model 修饰符

例如:

<MyInput v-model.trim="keyword" />

在 Vue 3 的组件里,你可以拿到对应的 modifier 信息并自行处理。它的意义是:不仅能双向绑定,还能把“如何修正输入值”的语义一起带给组件。

一句话总结 v-model

Vue 2 的 v-model 更像“约定俗成的一组默认 prop/event”; Vue 3 的 v-model 更像“一套统一的双向绑定协议”。

4. 自定义指令:Vue 3 把钩子名和组件生命周期对齐了

如果你写过 v-focusv-permissionv-lazy 这类自定义指令,Vue 3 的变化就必须记。

4.1 钩子名对照表

Vue 2Vue 3说明
bindbeforeMount指令绑定到元素,挂载前
insertedmounted元素插入 DOM 后
updateupdated / beforeUpdateVue 3 拆得更清楚
componentUpdatedupdated组件及其子树更新后
unbindunmounted指令解绑
createdVue 3 新增
beforeUpdateVue 3 新增
beforeUnmountVue 3 新增

更完整地说,Vue 3 自定义指令支持:

const myDirective = {
created(el, binding, vnode, prevVnode) {},
beforeMount(el, binding, vnode) {},
mounted(el, binding, vnode) {},
beforeUpdate(el, binding, vnode, prevVnode) {},
updated(el, binding, vnode, prevVnode) {},
beforeUnmount(el, binding, vnode) {},
unmounted(el, binding, vnode) {},
}

4.2 为什么要这样改?

因为 Vue 3 想让“组件”和“自定义指令”的时机命名更统一。

以前你需要额外记一套:

  • 组件:mountedupdateddestroyed
  • 指令:insertedcomponentUpdatedunbind

现在统一后,理解成本更低:

  • 都围绕 mount -> update -> unmount

4.3 迁移例子:v-focus

Vue 2:

Vue.directive('focus', {
inserted(el) {
el.focus()
},
})

Vue 3:

const app = createApp(App)

app.directive('focus', {
mounted(el) {
el.focus()
},
})

4.4 组件实例的获取方式变了

Vue 2 中,很多人会通过 vnode.context 拿组件实例。

Vue 3 中,推荐从 binding.instance 取:

const permissionDirective = {
mounted(el, binding) {
const vm = binding.instance
if (!vm) return
if (!binding.value) {
el.parentNode && el.parentNode.removeChild(el)
}
},
}

4.5 binding.expression 被移除了

Vue 2 的自定义指令里,binding.expression 可以拿到表达式字符串。

Vue 3 把它移除了,原因很直接:它不是运行时真正必须的信息,而且会增加实现复杂度。

日常开发里,更应该依赖这些字段:

  • binding.value
  • binding.oldValue
  • binding.arg
  • binding.modifiers
  • binding.instance

4.6 多根节点组件上,指令可能被忽略

Vue 3 支持 Fragment,也就是组件可以有多个根节点。这个时候如果你把自定义指令直接用在一个多根组件上,Vue 3 可能会忽略它并给出警告。

原因也很好理解:指令本质上是面向具体 DOM 元素的,多根组件没法唯一对应到一个根元素。

经验结论

如果一个需求明显依赖组件实例、插槽结构、多根节点协调,那它往往更适合抽成组件,而不是继续硬写成自定义指令。

5. v-ifv-for:同一元素上优先级变了

这是 Vue 3 很经典的破坏性变化。

5.1 Vue 2:v-for 优先

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

在 Vue 2 中,v-for 先执行,所以 v-if 可以访问 user

5.2 Vue 3:v-if 优先

Vue 3 中,同一元素上写 v-if + v-for,会先看 v-if。这会导致 v-if 阶段拿不到 v-for 创建出来的局部变量。

所以这类写法在 Vue 3 中容易出问题,也官方不推荐

5.3 正确改法:先过滤,再循环

最稳的写法是把过滤逻辑放到计算属性里:

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

<script setup>
import { computed } from 'vue'

const props = defineProps({
users: {
type: Array,
default: () => [],
},
})

const visibleUsers = computed(() => {
return props.users.filter(user => user.visible)
})
</script>
为什么这样更好?

因为模板只负责“展示”,过滤规则放到计算属性里后,可读性更高,也避免了 v-ifv-for 的优先级歧义。

6. key 相关规则:Vue 3 更强调“语义明确”

key 不是单独的指令,但它和 v-forv-if 这类模板指令强相关,迁移时很容易踩坑。

6.1 <template v-for>key 要写到 <template>

Vue 3 推荐这样写:

<template v-for="item in list" :key="item.id">
<div v-if="item.visible">{{ item.name }}</div>
<span v-else>隐藏</span>
</template>

不要再把 key 写在内部子元素上:

<!-- 不推荐 -->
<template v-for="item in list">
<div v-if="item.visible" :key="item.id">{{ item.name }}</div>
</template>

原因很简单:真正参与循环的是 <template v-for> 这一层,key 就应该挂在这一层。

6.2 条件分支上的 key:Vue 3 会自动生成唯一 key

Vue 2 里,很多资料会建议你手动写:

<div v-if="ok" key="yes">Yes</div>
<div v-else key="no">No</div>

Vue 3 中,即使你不写,框架也会为条件分支生成唯一 key。

所以更常见的写法是:

<div v-if="ok">Yes</div>
<div v-else>No</div>

如果你仍然想手写 key,那也必须保证每个分支都是不同的 key,不能再像某些 Vue 2 场景那样故意复用同一个 key 去强行复用节点。

7. v-bind="obj":Vue 3 改成“按出现顺序合并”

这个变化看起来小,但非常隐蔽。

7.1 Vue 2:单独属性总是优先

<div id="red" v-bind="{ id: 'blue' }"></div>

Vue 2 结果通常是:

<div id="red"></div>

也就是说,单独写的 id="red" 会覆盖对象里的 id: 'blue'

7.2 Vue 3:谁写得靠后,谁生效

同样的模板在 Vue 3 中,结果会变成:

<div id="blue"></div>

因为 Vue 3 认为:既然 v-bind="{ id: 'blue' }" 写在后面,那就应该以后面的结果为准。

如果你想让静态属性赢,那就把它写在后面:

<div v-bind="{ id: 'blue' }" id="red"></div>
高频坑

很多旧组件会写“默认 attrs 对象 + 局部覆盖”,升级到 Vue 3 后,如果顺序没注意,最终渲染结果可能和 Vue 2 不一致。

8. v-on 相关变化:.native 没了,keyCode 也没了

8.1 .native 被移除

Vue 2:

<MyButton @click.native="handleClick" />

这表示:监听的不是组件 $emit('click'),而是子组件根元素上的原生 DOM click

Vue 3 中 .native 被移除,直接写:

<MyButton @click="handleClick" />

前提是子组件要通过 emits 明确声明自己真正发出的组件事件。

<script>
export default {
emits: ['close'],
}
</script>

没有在 emits 中声明的监听器,Vue 3 会更倾向于把它当作原生监听器挂到子组件根节点上。

8.2 keyCode 修饰符被移除

Vue 2 里常见:

<input @keyup.13="submit" />

Vue 3 不再支持数字 keyCode,应该改成按键名:

<input @keyup.enter="submit" />
<input @keyup.page-down="nextPage" />

原因是浏览器层面的 KeyboardEvent.keyCode 本身就已经不推荐继续使用了。

9. filters 被移除,但它本来就不是“核心指令”

Vue 2 中你可能见过这样的模板:

{{ price | currency }}

Vue 3 里 filters 被移除了。

更推荐的替代方式有两种:

  1. 方法 / 计算属性
  2. 挂到 app.config.globalProperties 上统一调用

例如:

<template>
<p>{{ $filters.currencyUSD(price) }}</p>
</template>
const app = createApp(App)

app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
},
}
为什么要移除?

因为 filters 只是模板层糖衣,但它引入了额外语法,不如普通函数调用直观,也不利于统一表达式模型。

10. 还有哪些“容易被误以为是指令差异”的点?

这里专门做个澄清:

  • .sync 不是独立指令,而是 v-bind 的修饰语法
  • .native 不是独立指令,而是 v-on 的修饰符
  • filters 也不是 v-xxx 指令,而是模板表达式语法

但因为它们都直接出现在模板里,迁移时经常和“指令升级”一起被问到,所以面试或整理知识点时通常会放在一起讲。

11. Vue 2 -> Vue 3 指令迁移清单

如果你要迁一个老项目,可以按这个顺序扫:

  1. 全局搜索组件里的 v-model,确认子组件是否还在用 value / input
  2. .sync,改成 v-model:propName
  3. .native,删除它,并检查子组件 emits
  4. @keyup.13 这类 keyCode 修饰符,改成按键名
  5. 搜自定义指令定义,把 inserted/unbind/componentUpdated 等旧钩子改成新钩子名
  6. 搜同一元素上的 v-if + v-for,改成“先过滤、后循环”
  7. <template v-for> 的内部 key,移动到 <template>
  8. 搜 filters 管道写法,改为函数/计算属性
  9. 检查 v-bind="obj" 与静态属性混用的地方,确认覆盖顺序是否符合预期

12. 面试高频问答

Q1:Vue 2 和 Vue 3 在指令层面最大的区别是什么?

最大的区别不是“基础指令全部重写了”,而是少数关键指令的协议和规则变了。最典型的是:

  • 组件上的 v-modelvalue/input 改成了 modelValue/update:modelValue
  • 自定义指令生命周期钩子改名,并与组件生命周期对齐
  • v-ifv-for 在同一元素上的优先级从“v-for 优先”变成了“v-if 优先”

Q2:为什么 Vue 3 要重做 v-model

因为 Vue 2 的 v-model + .sync + model 选项组合起来,能力虽然够用,但规则分散。Vue 3 把它统一成一套更完整的双向绑定协议:

  • 默认 modelValue
  • 标准事件 update:modelValue
  • 支持 v-model:xxx
  • 支持多个 v-model
  • 支持自定义 modifier

这样语义更统一,也更适合组件库开发。

Q3:Vue 3 为什么不建议把 v-ifv-for 写在同一个元素上?

因为 Vue 3 中 v-if 优先级更高,会先于 v-for 执行,容易导致 v-if 阶段拿不到循环变量。更重要的是,这种写法本身可读性就差。更好的做法是把过滤逻辑移到计算属性里,再让模板只负责渲染。

Q4:自定义指令在 Vue 3 里最大的变化是什么?

一是钩子名改了,二是心智模型更接近组件生命周期。你可以直接按 created -> mounted -> updated -> unmounted 去记。除此之外,获取组件实例不再走 vnode.context,而是走 binding.instancebinding.expression 也被移除了。

Q5:.native 去掉后,父组件怎么监听子组件根元素的原生事件?

Vue 3 里直接写 @click 即可,但前提是子组件要通过 emits 明确声明自己发出的组件事件。没有声明为组件事件的监听器,会更自然地落到子组件根元素上。

Q6:v-bind="obj" 的变化为什么危险?

因为它不是报错型问题,而是“渲染结果悄悄变了”。Vue 2 是“单独属性优先”,Vue 3 是“后写的优先”。升级后页面不一定直接炸,但属性覆盖顺序可能变,尤其在封装基础组件、透传 attrs 时最容易出隐性 Bug。

13. 总结

把这篇文章压缩成一句话就是:

Vue 2 到 Vue 3,指令层面最值得记的不是“名字变了多少”,而是“模板协议更统一、边界更明确、歧义更少了”。

你真正要重点掌握的是:

  • v-model 统一成 modelValue + update:modelValue
  • 自定义指令钩子改为贴近组件生命周期
  • v-if/v-for 优先级变化
  • keyv-bind="obj" 的规则更严格
  • .native、keyCode modifiers、filters 这些旧语法要学会迁移

如果你能把这几块讲清楚,Vue 2 / Vue 3 的“指令区别”这一题,基本就答得很完整了。