Vue 2 / Vue 3 指令的区别:迁移重点与常见坑
很多同学一看到“Vue 2 升 Vue 3 的指令差异”,脑子里会同时冒出一堆问题:
v-model到底变了多少?.sync、.native、filters 还在吗?- 自定义指令为什么钩子名全改了?
v-if和v-for写在一起,为什么 Vue 3 表现不一样?
这篇文章不打算把所有指令从头背一遍,而是专门讲清楚:Vue 2 和 Vue 3 在“指令相关用法”上,哪些没变,哪些变了,为什么变,以及迁移时该怎么改。
1. 先给结论:真正要重点记的差异只有这几类
| 类别 | Vue 2 | Vue 3 | 影响程度 |
|---|---|---|---|
组件上的 v-model | 默认是 value + input | 默认是 modelValue + update:modelValue | 非常高 |
多个 v-model | 不自然,常配合 .sync | 原生支持 v-model:xxx | 非常高 |
| 自定义指令钩子 | bind/inserted/update/componentUpdated/unbind | created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted | 高 |
v-if 和 v-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-elsev-showv-forv-bindv-onv-textv-htmlv-prev-cloakv-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-modelVue 2 的 v-model 更像“约定俗成的一组默认 prop/event”;
Vue 3 的 v-model 更像“一套统一的双向绑定协议”。
4. 自定义指令:Vue 3 把钩子名和组件生命周期对齐了
如果你写过 v-focus、v-permission、v-lazy 这类自定义指令,Vue 3 的变化就必须记。
4.1 钩子名对照表
| Vue 2 | Vue 3 | 说明 |
|---|---|---|
bind | beforeMount | 指令绑定到元素,挂载前 |
inserted | mounted | 元素插入 DOM 后 |
update | updated / beforeUpdate | Vue 3 拆得更清楚 |
componentUpdated | updated | 组件及其子树更新后 |
unbind | unmounted | 指令解绑 |
| 无 | created | Vue 3 新增 |
| 无 | beforeUpdate | Vue 3 新增 |
| 无 | beforeUnmount | Vue 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 想让“组件”和“自定义指令”的时机命名更统一。
以前你需要额外记一套:
- 组件:
mounted、updated、destroyed - 指令:
inserted、componentUpdated、unbind
现在统一后,理解成本更低:
- 都围绕
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.valuebinding.oldValuebinding.argbinding.modifiersbinding.instance
4.6 多根节点组件上,指令可能被忽略
Vue 3 支持 Fragment,也就是组件可以有多个根节点。这个时候如果你把自定义指令直接用在一个多根组件上,Vue 3 可能会忽略它并给出警告。
原因也很好理解:指令本质上是面向具体 DOM 元素的,多根组件没法唯一对应到一个根元素。
如果一个需求明显依赖组件实例、插槽结构、多根节点协调,那它往往更适合抽成组件,而不是继续硬写成自定义指令。
5. v-if 和 v-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-if 和 v-for 的优先级歧义。
6. key 相关规则:Vue 3 更强调“语义明确”
key 不是单独的指令,但它和 v-for、v-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 被移除了。
更推荐的替代方式有两种:
- 方法 / 计算属性
- 挂到
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 指令迁移清单
如果你要迁一个老项目,可以按这个顺序扫:
- 全局搜索组件里的
v-model,确认子组件是否还在用value/input - 搜
.sync,改成v-model:propName - 搜
.native,删除它,并检查子组件emits - 搜
@keyup.13这类 keyCode 修饰符,改成按键名 - 搜自定义指令定义,把
inserted/unbind/componentUpdated等旧钩子改成新钩子名 - 搜同一元素上的
v-if+v-for,改成“先过滤、后循环” - 搜
<template v-for>的内部key,移动到<template>上 - 搜 filters 管道写法,改为函数/计算属性
- 检查
v-bind="obj"与静态属性混用的地方,确认覆盖顺序是否符合预期
12. 面试高频问答
Q1:Vue 2 和 Vue 3 在指令层面最大的区别是什么?
最大的区别不是“基础指令全部重写了”,而是少数关键指令的协议和规则变了。最典型的是:
- 组件上的
v-model从value/input改成了modelValue/update:modelValue - 自定义指令生命周期钩子改名,并与组件生命周期对齐
v-if和v-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-if 和 v-for 写在同一个元素上?
因为 Vue 3 中 v-if 优先级更高,会先于 v-for 执行,容易导致 v-if 阶段拿不到循环变量。更重要的是,这种写法本身可读性就差。更好的做法是把过滤逻辑移到计算属性里,再让模板只负责渲染。
Q4:自定义指令在 Vue 3 里最大的变化是什么?
一是钩子名改了,二是心智模型更接近组件生命周期。你可以直接按 created -> mounted -> updated -> unmounted 去记。除此之外,获取组件实例不再走 vnode.context,而是走 binding.instance;binding.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优先级变化key和v-bind="obj"的规则更严格.native、keyCode modifiers、filters 这些旧语法要学会迁移
如果你能把这几块讲清楚,Vue 2 / Vue 3 的“指令区别”这一题,基本就答得很完整了。