Vue 2 与 Vue 3 指令的区别
“Vue 2 和 Vue 3 指令有什么区别”这个题,很多人会直接答成“v-model 变了”。这没错,但不完整。
更严谨地说,这道题至少有三层:
- 内置指令语义差异
- 自定义指令生命周期差异
- 模板编译规则变化对指令行为的影响
面试速答(30 秒版 TL;DR)
- 最常考的变化有四个:
- 组件上的
v-model协议变了:value/input->modelValue/update:modelValue .sync被移除,统一收敛到v-model:arg- 自定义指令钩子名改成更接近组件生命周期:
bind->beforeMount,inserted->mounted等 v-if和v-for的优先级、key在template 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 自定义指令钩子:
bindinsertedupdatecomponentUpdatedunbind
Vue 3 改成:
beforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted
对照关系如下:
| Vue 2 | Vue 3 |
|---|---|
bind | beforeMount |
inserted | mounted |
update | beforeUpdate |
componentUpdated | updated |
unbind | unmounted |
为什么这么改?
因为 Vue 3 统一了组件和指令的生命周期命名体系,理解成本更低,也更一致。
4. 自定义指令上下文对象也有变化
Vue 2 里你可能会用:
vnode.contextbinding.expression
Vue 3 里常见变化是:
binding.expression被移除- 组件实例通过
binding.instance获取
所以老指令迁移时,不能只改钩子名,很多上下文字段也要一起检查。
5. v-if 和 v-for 的优先级变化
这点很容易被忽略,但面试挺爱问。
- Vue 2:
v-for优先级高于v-if - Vue 3:
v-if优先级高于v-for
这意味着:
<li v-for="item in list" v-if="item.visible" :key="item.id">
{{ item.name }}
</li>
在两代里的编译结果和变量可见性理解会不一样。
实践建议
不要把 v-if 和 v-for 写在同一个元素上,推荐改写为:
<template v-for="item in visibleList" :key="item.id">
<li>{{ item.name }}</li>
</template>
或者在计算属性里先过滤再渲染。
6. template v-for 的 key 放置方式变化
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-if 和 v-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-model:value/input->modelValue/update:modelValue.sync:移除,统一到v-model:arg- 指令钩子:改成组件生命周期风格
v-if/v-for:优先级变化template v-for:key放在template上更符合 Vue 3 语义