Vue3面试必问?Composition API到底比Options API好在哪,怎么用不出错?
先别急着背优势,得搞懂面试官为啥揪着这个问
刚上手Vue3的人,第一个逃不开的选择大概率是用老的Options还是新的Composition?很多面经会列一堆优势比如更好复用、更灵活,但如果只会干巴巴念这些,面试官可能追问两句就露馅——比如问你项目里遇到过什么痛点,Options解决不了只能用Composition的?或者有没有Composition用多了反而搞出bug的场景? 面试官真正想挖的,其实是你对两种API底层逻辑的理解,有没有实际踩过坑,能不能根据项目规模和需求场景选对工具,而不是跟风全改Composition,毕竟现在很多大厂项目还是Vue2升级的混合模式,完全纯Options或者纯Composition的都有,但会分析场景、合理使用才是关键能力。
第一个对比核心:代码组织逻辑,不是代码行数!
很多人说“Composition API代码更短”,这其实是个误区——简单的Todo List,两种API写出来的代码量差不了多少,甚至Options更直观,真正的差异,是代码是按“功能模块”凑在一起,还是按“选项类型”分散摆放。
举个真实的例子,我之前做过一个学生课表的小组件,Options API版本的代码是这样的: (代码可以简单写逻辑要点,但别全贴太繁琐的示例)
- 在
data()里定义了课表数组、选中日期、课程弹窗显示状态、搜索关键词 - 在
methods里写了切换日期、请求课表、打开/关闭弹窗、搜索课程 - 在
computed里写了当天已上课/待上课筛选、课程时间格式化 - 在
watch里监听选中日期变化重新请求 - 在
mounted里初始化选中日期、请求初始课表
看起来每个选项都很清晰?但课表功能还要加提醒——比如上课前10分钟弹出通知框,那这个提醒相关的代码要拆到哪里?定时器放mounted里开启、beforeUnmount里销毁?提醒状态放data里?提醒触发逻辑放watch监听选中日期的待上课筛选结果里?
这时候,一个完整的“课表提醒功能”就被拆得七零八落,改bug或者新增功能时,得在整个文件里跳来跳去找相关的data、methods、computed、watch,组件超过200行就特别崩溃——我之前改这个课表组件的提醒音效时,先找音效播放的methods,再找待上课课程筛选的computed,最后才发现watch里漏加了音效触发的条件,折腾了20多分钟。
那用Composition API重构后呢?
可以把所有和课表查询相关的逻辑放在一个自定义Hook(比如useCourseTableQuery)里,把所有和提醒相关的逻辑放在另一个自定义Hook(比如useCourseReminder)里,把所有和弹窗交互相关的逻辑放在useModal里,这样每个功能模块的代码是自包含、可复用、逻辑集中的,哪怕后来课表组件要新增搜索筛选条件或者换提醒方式,只需要改对应的自定义Hook就行,完全不用碰其他模块的代码。
重构后的课表组件主文件只剩什么?只剩import这些Hook,然后解构出需要的响应式变量、方法,往模板里一塞,整个主文件可能就50行不到,一眼就能看完整个组件的结构。
第二个对比核心:复用能力,自定义Hook是真的香
Options API也有复用逻辑的方法,比如Mixin和高阶组件(HOC),但这俩都有致命的缺陷。
先说说Mixin:
- 命名冲突问题太严重:比如你用了一个别人写的
useLocalStorageMixinMixin,里面定义了data里的storageKey,然后你自己的组件里也需要storageKey变量存课程缓存的key,这时候谁覆盖谁?如果Mixin里还定义了getStorageData方法,你组件里也有同名方法,那你的方法会覆盖Mixin的,还是反过来?虽然Vue2有规则,但规则多了记不住,很容易出bug。 - 代码来源不透明:当组件里用了3个以上的Mixin时,你根本不知道某个响应式变量或者方法是从哪个Mixin里来的,得一个一个Mixin去翻代码,太浪费时间。
- 逻辑耦合问题:Mixin里的逻辑有时候是绑定在一起的,比如你只想用
useLocalStorageMixin里的getStorageData方法,但不得不把它整个引入进来,包括它定义的storageKey、saveStorageData这些你根本用不上的变量和方法,增加了组件的冗余。
再说说高阶组件(HOC):
- 模板层级嵌套过深:HOC通常是用组件包裹组件,比如你要给组件加权限控制,又要加加载状态,又要加数据缓存,那最终渲染的DOM里可能会有3-4层无意义的div包裹(虽然Vue3有Teleport和Fragments,但之前的Vue2或者很多旧的HOC库还是会有这个问题),不仅影响性能,还会让浏览器的DevTools变得很乱,调试起来不方便。
- Props和事件透传麻烦:HOC如果要把父组件的props和事件透传给子组件,得手动写
v-bind="$attrs"和v-on="$listeners",还要处理inheritAttrs的问题,稍微漏了一个就会出bug。 - 类型推断差:TypeScript和HOC的兼容性一直不好,很多时候要手动写类型声明,代码量增加了不少,还容易写错。
那Composition API的自定义Hook呢?完美解决了这些问题:
- 命名冲突可以通过解构重命名解决:比如
import { useLocalStorage as useCourseStorage } from './hooks',或者解构的时候const { storageKey: courseStorageKey, getStorageData } = useCourseStorage(),完全不用担心和其他Hook或者组件本身的变量重名。 - 代码来源非常透明:每个响应式变量和方法都是从对应的自定义Hook里解构出来的,鼠标悬停在变量名上(如果用了TypeScript或者Vetur/Volar),还能直接看到它的类型和定义位置,调试和维护都特别方便。
- 逻辑可以按需引入:你可以把自定义Hook写得很细,比如
useLocalStorage只负责存和取单个key的数据,useDebouncedWatch只负责防抖监听某个变量,需要什么功能就引入什么功能,不会有任何冗余。 - 类型推断完美:Vue3的Composition API是用TypeScript优先设计的,自定义Hook的类型推断非常流畅,几乎不用手动写类型声明,或者只需要写一点点泛型就够了。
- 没有模板层级嵌套:自定义Hook只是纯JavaScript/TypeScript函数,完全不会影响DOM结构,DevTools也能清晰地看到每个响应式变量的变化。
举个复用的小例子,我把之前课表组件里的useLocalStorageHook抽出来,后来在用户偏好设置组件里也用到了——存用户的主题色、字体大小、是否开启提醒,完全不用改任何代码,直接引入就行,省了很多时间。
第三个对比核心:TypeScript支持,这才是Composition API的“杀手锏”
现在很多大厂的前端项目都要求用TypeScript,Options API虽然也能写TypeScript,但体验真的很差。
先说说Options API写TypeScript的痛点:
data()返回的对象类型推断不出来:得手动给data()加一个泛型或者接口,interface CourseTableData { courseList: Course[]; selectedDate: string; isModalOpen: boolean; }
export default defineComponent({ data(): CourseTableData { return { courseList: [], selectedDate: new Date().toISOString().split('T')[0], isModalOpen: false }; } });
- **`methods`、`computed`、`watch`里的`this`类型不稳定**:有时候Vetur/Volar会推断不出来`this`的类型,得手动用`as this`强转,或者给组件加一个`ComponentPublicInstance`的泛型,非常麻烦。
- **Mixin和TypeScript完全不兼容**:如果组件里用了Mixin,那Mixin里的变量和方法的类型根本推断不出来,只能全写`any`,失去了TypeScript的意义。
那Composition API写TypeScript呢?体验简直是“飞一般的感觉”:
- **`ref`、`reactive`的类型推断自动完成**:const selectedDate = ref(new Date().toISOString().split('T')[0])`,Volar会自动推断出`selectedDate`的类型是`Ref<string>`;`const courseList = reactive<Course[]>([])`,只需要加一个简单的泛型就行。
- **没有`this`的困扰**:Composition API里不需要用`this`,所有的变量和方法都是直接定义的,类型推断非常流畅,几乎不会出问题。
- **自定义Hook的类型推断完美**:只要你的自定义Hook里用了正确的泛型,解构出来的变量和方法的类型就会自动传过来,
```typescript
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
export function useLocalStorage<T>(key: string, defaultValue: T) {
const data = ref<T>(defaultValue);
const loadData = () => {
const stored = localStorage.getItem(key);
if (stored) {
try {
data.value = JSON.parse(stored) as T;
} catch {
data.value = defaultValue;
}
}
};
const saveData = () => {
localStorage.setItem(key, JSON.stringify(data.value));
};
watch(data, saveData, { deep: true });
onMounted(loadData);
return { data, loadData, saveData };
}
然后在组件里用的时候:
const { data: themeColor } = useLocalStorage<'light' | 'dark'>('theme-color', 'light');
Volar会自动推断出themeColor的类型是Ref<'light' | 'dark'>,完全不用手动写。
别吹Composition API上天,它也有适用场景和“坑”
很多人觉得Vue3就应该全用Composition API,其实不是的,两种API各有各的适用场景:
- Options API的适用场景:
- 简单的小组件:比如按钮、输入框、标签页,逻辑不复杂,代码量少,用Options API更直观,上手更快。
- Vue2升级的混合模式项目:如果项目是从Vue2升级过来的,有很多旧的Options API组件,这时候没必要全改成Composition API,保持混合模式就行,新组件可以用Composition API,旧组件慢慢迭代。
- 新手学习Vue:Options API的选项分类很清晰,新手可以快速理解Vue的核心概念(data、methods、computed、watch、生命周期钩子),等理解透了再学Composition API会更轻松。
- Composition API的适用场景:
- 复杂的业务组件:比如课表、图表、表单提交,逻辑模块多,需要复用很多功能,用Composition API可以更好地组织代码。
- 需要大量复用逻辑的项目:比如电商平台的商品筛选、购物车、订单管理,这些功能在很多页面都能用到,抽成自定义Hook可以省很多时间。
- 用TypeScript的项目:Composition API和TypeScript的兼容性最好,体验最佳。
接下来要说Composition API的“坑”了,这些坑面试时也经常被问到,踩过的话答出来会很加分:
坑一:ref和reactive的区别搞不清,乱用导致响应式失效
很多新手刚学Composition API时,会把ref和reactive搞混,
- 用reactive定义基本类型:
const count = reactive(0),这根本不会有响应式,因为reactive只能定义对象或数组,基本类型要用ref。 - 用ref定义对象后,直接替换整个对象:
const user = ref({ name: '张三', age: 18 }); user = { name: '李四', age: 20 },这也不会有响应式,因为你替换的是ref本身,而不是ref.value,应该写成user.value = { name: '李四', age: 20 }。 - 解构reactive对象后失去响应式:
const state = reactive({ count: 0, name: '张三' }); const { count, name } = state,这时候count和name变成了普通变量,不会有响应式,应该用toRefs或者toRef:const { count, name } = toRefs(state)。
那怎么正确区分ref和reactive呢?这里有个简单的小技巧:基本类型用ref,对象/数组尽量也用ref(除非你确定不会替换整个对象),为什么对象/数组尽量也用ref?因为ref定义的对象/数组,修改时只需要加.value,替换整个对象时也只要加.value,不容易出错;而reactive定义的对象/数组,虽然修改属性不需要加.value,但替换整个对象时需要用Object.assign或者直接解构再重新赋值,反而更麻烦,而且新手很容易忘记。
坑二:生命周期钩子的执行顺序搞不清,导致逻辑错误
Vue3的生命周期钩子和Vue2的差不多,只是改了名字:
- Vue2的
beforeCreate和created合并成了setup()函数本身(setup执行时,组件实例还没创建,所以不能用this) - Vue2的
beforeMount→ Vue3的onBeforeMount - Vue2的
mounted→ Vue3的onMounted - Vue2的
beforeUpdate→ Vue3的onBeforeUpdate - Vue2的
updated→ Vue3的onUpdated - Vue2的
beforeDestroy→ Vue3的onBeforeUnmount - Vue2的
destroyed→ Vue3的onUnmounted
除了这些,Vue3还新增了一些生命周期钩子,比如onActivated(keep-alive组件激活时)、onDeactivated(keep-alive组件停用时)、onErrorCaptured(捕获子孙组件的错误)。
很多新手容易犯的错误是,在setup()函数里直接写异步请求,
setup() {
const courseList = ref<Course[]>([]);
// 错误写法:直接在setup里写async函数,会导致setup返回一个Promise,模板里的变量暂时无法使用
async function getCourseList() {
const res = await fetch('/api/course-list');
courseList.value = await res.json();
}
getCourseList();
return { courseList };
}
虽然这种写法在Vue3.2+里已经被优化了(模板里会有一个默认的pending状态),但更好的写法是把异步请求放在onMounted里,或者用watch监听某个变量触发请求,这样可以更好地控制请求的时机。
还有一个坑是,在自定义Hook里用生命周期钩子时,要确保自定义Hook是在setup()函数或者另一个自定义Hook里调用的,否则生命周期钩子不会生效——因为Vue3的生命周期钩子是依赖组件实例的,只有在组件实例创建的过程中调用,才会绑定到对应的组件上。
坑三:响应式数据的浅层监听和深层监听搞不清,导致性能问题或者逻辑错误
Vue3的watch默认是浅层监听的,比如你用watch监听一个ref定义的对象,只有当你替换整个对象时才会触发回调,修改对象的属性是不会触发的;如果用watch监听一个reactive定义的对象,默认是深层监听的,修改对象的任何属性都会触发回调。
很多新手不知道这一点,
- 用
watch监听一个ref定义的大对象,然后忘记加{ deep: true },导致修改属性时回调不触发,逻辑错误。 - 用
watch监听一个reactive定义的大对象,导致每次修改任何一个小属性都会触发回调,性能很差。
那怎么正确使用浅层监听和深层监听呢?这里有个原则:能不用深层监听就不用深层监听,如果只需要监听对象的某个属性,可以直接监听那个属性,比如watch(() => state.user.name, (newVal) => { ... });如果需要监听对象的多个属性,可以用数组,比如watch([() => state.user.name, () => state.user.age], ([newName, newAge]) => { ... });如果必须监听整个对象的所有属性变化,再考虑加{ deep: true },但这时候要注意性能问题,可以加{ flush: 'post' }让回调在DOM更新后再执行,或者加{ immediate: true }让回调在初始化时就执行一次。
还有一个优化性能的API是watchEffect,它会自动收集依赖,不需要手动指定监听的变量,
watchEffect(() => {
console.log(state.user.name, state.user.age);
});
当state.user.name或者state.user.age变化时,watchEffect的回调会自动触发,非常方便,但watchEffect的缺点是不能获取旧值,而且默认是在DOM更新前执行的,如果需要获取旧值或者控制执行时机,还是用watch更好。
面试时怎么答这个问题?别光背,要结合实际项目!
如果面试时被问到“Composition API到底比Options API好在哪,怎么用不出错?”,你可以按照这个思路回答:
- 先铺垫:两种API没有绝对的好坏,只有适用场景的不同,不能跟风全改Composition API。
- 再讲优势(结合实际项目的例子):
- 代码组织逻辑:从按选项分类到按功能模块分类,举之前的课表组件例子,说明Mixin拆分功能的痛点和自定义Hook组织功能的好处。
- 复用能力:对比Mixin、HOC和自定义Hook,举抽离
useLocalStorageHook复用的例子。 - TypeScript支持:对比Options API和Composition API写TypeScript的体验,举ref、reactive、自定义Hook的类型推断例子。
- 然后讲适用场景:简单说明什么时候用Options,什么时候用Composition。
- 最后讲踩过的坑和解决方法(加分项!):讲1-2个你实际踩过的坑,比如ref和reactive的区别、watch的深层监听,然后说你是怎么解决的,以及总结出的原则。
这样回答,不仅有理论,还有实际项目的例子,还有踩坑经验,面试官肯定会对你印象深刻。
最后总结一下
Vue3的Composition API是一个非常棒的工具,但它不是“万能药”,也不是用来替代Options API的,而是用来补充Options API的,在实际开发中,我们应该根据项目的规模、需求场景、团队的技术栈来选择合适的API——简单的小组件用Options API,复杂的业务组件用Composition API,Vue2升级的项目用混合模式,用TypeScript的项目优先用Composition API,我们还要注意Composition API的一些“坑”,比如ref和reactive的区别、生命周期钩子的执行顺序、响应式数据的浅层监听和深层监听,避免踩坑影响开发效率和代码质量。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


