组合式API
组合式API(Composition API)是Vue 3中引入的新特性,它提供了一种新的组织组件逻辑的方式,使代码更加可维护和可复用。本文将详细介绍Vue 3的组合式API及其使用场景。
组合式API的背景
在Vue 2中,我们使用选项式API(Options API)来组织组件逻辑,将代码分为data、methods、computed等选项。这种方式对于小型组件来说非常直观,但对于大型组件来说,逻辑会分散在不同的选项中,导致代码难以维护和复用。
组合式API的出现就是为了解决这个问题,它允许我们按照逻辑关注点组织代码,而不是按照选项类型。
组合式API的基本使用
1. setup函数
setup函数是组合式API的入口点,它在组件创建之前执行,替代了Vue 2中的beforeCreate和created钩子。
使用方式:
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
// 响应式数据
const count = ref(0);
// 计算属性
const title = computed(() => `Count: ${count.value}`);
// 方法
const increment = () => {
count.value++;
};
// 生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
// 返回需要暴露给模板的变量和方法
return {
count,
title,
increment
};
}
};
</script>注意:在setup函数中,this关键字不可用,因为setup函数在组件实例创建之前执行。
2. 响应式API
组合式API提供了以下响应式API:
2.1 ref
ref用于创建响应式的基本类型数据(String、Number、Boolean等)。
使用方式:
import { ref } from 'vue';
// 创建响应式数据
const count = ref(0);
// 访问响应式数据
console.log(count.value); // 0
// 修改响应式数据
count.value++; // 1特点:
ref返回一个包含value属性的对象。- 在模板中使用
ref创建的响应式数据时,Vue会自动解包,不需要使用.value。 - 在
setup函数中使用ref创建的响应式数据时,需要使用.value访问和修改。
2.2 reactive
reactive用于创建响应式的对象类型数据。
使用方式:
import { reactive } from 'vue';
// 创建响应式对象
const user = reactive({
name: 'John',
age: 30
});
// 访问响应式对象的属性
console.log(user.name); // John
// 修改响应式对象的属性
user.age = 31;特点:
reactive返回一个响应式的代理对象。- 不能直接替换整个响应式对象,否则会失去响应性。
- 可以使用
toRefs将响应式对象转换为ref对象。
2.3 toRefs
toRefs用于将响应式对象转换为ref对象的集合,这样在解构时不会失去响应性。
使用方式:
import { reactive, toRefs } from 'vue';
// 创建响应式对象
const user = reactive({
name: 'John',
age: 30
});
// 转换为ref对象的集合
const userRefs = toRefs(user);
// 解构
const { name, age } = userRefs;
// 访问和修改
console.log(name.value); // John
age.value = 31;2.4 computed
computed用于创建计算属性,计算属性会缓存计算结果,只有当依赖项变化时才会重新计算。
使用方式:
import { ref, computed } from 'vue';
const count = ref(0);
// 创建计算属性
const doubleCount = computed(() => {
return count.value * 2;
});
// 访问计算属性
console.log(doubleCount.value); // 0
// 修改依赖项
count.value = 1;
console.log(doubleCount.value); // 2特点:
computed返回一个只读的ref对象。- 计算属性的函数应该是纯函数,不应该修改状态。
2.5 watch
watch用于监听响应式数据的变化。
使用方式:
import { ref, watch } from 'vue';
const count = ref(0);
// 监听单个数据
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// 监听多个数据
watch([count1, count2], ([newCount1, newCount2], [oldCount1, oldCount2]) => {
console.log('Counts changed');
});
// 监听对象的属性
watch(
() => user.name,
(newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
}
);
// 深度监听
watch(
user,
(newUser, oldUser) => {
console.log('User changed');
},
{ deep: true }
);2.6 watchEffect
watchEffect用于自动追踪响应式依赖的变化,当依赖项变化时,会重新执行回调函数。
使用方式:
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('John');
// 自动追踪依赖
const stop = watchEffect(() => {
console.log(`Count: ${count.value}, Name: ${name.value}`);
});
// 修改依赖项
count.value = 1; // 会触发回调
name.value = 'Jane'; // 会触发回调
// 停止监听
stop();特点:
watchEffect会立即执行一次回调函数,然后自动追踪依赖。watchEffect返回一个停止函数,可以手动停止监听。
3. 生命周期钩子
组合式API提供了以下生命周期钩子:
| 组合式API钩子 | 选项式API钩子 | 描述 |
|---|---|---|
| onBeforeMount | beforeMount | 组件挂载之前执行 |
| onMounted | mounted | 组件挂载之后执行 |
| onBeforeUpdate | beforeUpdate | 组件更新之前执行 |
| onUpdated | updated | 组件更新之后执行 |
| onBeforeUnmount | beforeDestroy | 组件卸载之前执行 |
| onUnmounted | destroyed | 组件卸载之后执行 |
| onActivated | activated | keep-alive组件激活时执行 |
| onDeactivated | deactivated | keep-alive组件停用时执行 |
| onErrorCaptured | errorCaptured | 捕获子组件错误时执行 |
| onRenderTracked | - | 响应式依赖被收集时执行(仅开发模式) |
| onRenderTriggered | - | 响应式依赖被触发时执行(仅开发模式) |
使用方式:
import { onMounted, onBeforeUnmount } from 'vue';
setup() {
onMounted(() => {
console.log('Component mounted');
});
onBeforeUnmount(() => {
console.log('Component before unmount');
});
}4. 依赖注入
组合式API提供了provide和inject函数,用于跨组件传递数据。
使用方式:
// 父组件
import { provide, ref } from 'vue';
export default {
setup() {
const message = ref('Hello from parent');
// 提供数据
provide('message', message);
return {};
}
};
// 子组件
import { inject } from 'vue';
export default {
setup() {
// 注入数据
const message = inject('message');
return {
message
};
}
};组合式API的优势
1. 更好的代码组织
组合式API允许我们按照逻辑关注点组织代码,而不是按照选项类型。这样可以使代码更加清晰易读,便于维护。
示例:
// 选项式API
export default {
data() {
return {
count: 0,
user: {}
};
},
methods: {
increment() {
this.count++;
},
fetchUser() {
// 获取用户数据
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
mounted() {
this.fetchUser();
}
};
// 组合式API
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
// 计数逻辑
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
// 用户逻辑
const user = ref({});
const fetchUser = async () => {
// 获取用户数据
};
// 生命周期
onMounted(() => {
fetchUser();
});
return {
count,
doubleCount,
increment,
user
};
}
};2. 更好的代码复用
组合式API允许我们将相关的逻辑提取到单独的函数中,这些函数可以在多个组件中复用。
示例:
// composables/useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
return {
count,
doubleCount,
increment,
decrement
};
}
// 组件中使用
import { useCounter } from '../composables/useCounter';
export default {
setup() {
const { count, doubleCount, increment, decrement } = useCounter(10);
return {
count,
doubleCount,
increment,
decrement
};
}
};3. 更好的类型推断
组合式API提供了更好的TypeScript类型推断支持,因为它使用了函数式的方式组织代码,使得TypeScript能够更好地推断类型。
示例:
// composables/useCounter.ts
import { ref, computed, Ref } from 'vue';
export function useCounter(initialValue: number = 0) {
const count: Ref<number> = ref(initialValue);
const doubleCount: Ref<number> = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
return {
count,
doubleCount,
increment,
decrement
};
}
// 组件中使用
import { useCounter } from '../composables/useCounter';
export default {
setup() {
const { count, doubleCount, increment, decrement } = useCounter(10);
// TypeScript会自动推断出count的类型为Ref<number>
return {
count,
doubleCount,
increment,
decrement
};
}
};4. 更小的打包体积
组合式API支持Tree Shaking,只打包使用到的API,减少了打包体积。
组合式API的实际应用
1. 状态管理
使用组合式API可以创建简单的状态管理方案,而不需要使用Vuex或Pinia。
示例:
// composables/useStore.js
import { ref, computed } from 'vue';
const count = ref(0);
const user = ref({});
export function useStore() {
const increment = () => count.value++;
const setUser = (newUser) => user.value = newUser;
const doubleCount = computed(() => count.value * 2);
return {
count,
user,
increment,
setUser,
doubleCount
};
}2. 表单处理
使用组合式API可以创建可复用的表单处理逻辑。
示例:
// composables/useForm.js
import { ref, computed } from 'vue';
export function useForm(initialValues = {}) {
const values = ref({ ...initialValues });
const errors = ref({});
const touched = ref({});
const validate = () => {
errors.value = {};
// 验证逻辑
return Object.keys(errors.value).length === 0;
};
const handleSubmit = (onSubmit) => {
touched.value = Object.fromEntries(
Object.keys(values.value).map(key => [key, true])
);
if (validate()) {
onSubmit(values.value);
}
};
const handleChange = (key, value) => {
values.value[key] = value;
if (touched.value[key]) {
// 验证单个字段
}
};
const handleBlur = (key) => {
touched.value[key] = true;
// 验证单个字段
};
const isError = (key) => touched.value[key] && errors.value[key];
return {
values,
errors,
touched,
validate,
handleSubmit,
handleChange,
handleBlur,
isError
};
}3. 网络请求
使用组合式API可以创建可复用的网络请求逻辑。
示例:
// composables/useFetch.js
import { ref, onMounted, watch } from 'vue';
export function useFetch(url, options = {}) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('Network response was not ok');
}
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
// 监听url变化
watch(url, () => {
fetchData();
});
return {
data,
loading,
error,
refetch: fetchData
};
}组合式API与选项式API的对比
| 特性 | 组合式API | 选项式API |
|---|---|---|
| 代码组织 | 按逻辑关注点组织 | 按选项类型组织 |
| 代码复用 | 支持逻辑复用,可提取为单独的函数 | 支持mixins,但可能导致命名冲突 |
| 类型推断 | 更好的TypeScript类型推断 | 类型推断较差 |
| 打包体积 | 支持Tree Shaking,打包体积更小 | 不支持Tree Shaking,打包体积较大 |
| 学习曲线 | 学习曲线较陡 | 学习曲线较平缓 |
| 适用场景 | 大型组件,需要逻辑复用的场景 | 小型组件,逻辑简单的场景 |
组合式API的最佳实践
1. 按逻辑关注点组织代码
使用组合式API时,应该按照逻辑关注点组织代码,将相关的状态、计算属性和方法放在一起。
2. 提取可复用的逻辑
将可复用的逻辑提取为单独的composables函数,以便在多个组件中复用。
3. 使用TypeScript
组合式API与TypeScript配合使用可以获得更好的类型推断支持,提高代码的可维护性。
4. 合理使用响应式API
- 对于基本类型数据,使用
ref。 - 对于对象类型数据,使用
reactive。 - 对于需要解构的响应式对象,使用
toRefs。
5. 避免在setup函数中使用this
在setup函数中,this关键字不可用,应该使用组合式API提供的响应式API和生命周期钩子。
6. 合理使用watch和watchEffect
- 对于需要明确知道新旧值的场景,使用
watch。 - 对于需要自动追踪依赖的场景,使用
watchEffect。
面试常见问题
1. 什么是组合式API?它解决了什么问题?
组合式API是Vue 3中引入的新特性,它提供了一种新的组织组件逻辑的方式,使代码更加可维护和可复用。
解决的问题:
- 代码组织:选项式API按选项类型组织代码,导致大型组件的逻辑分散,难以维护。
- 代码复用:选项式API通过mixins实现代码复用,但可能导致命名冲突。
- 类型推断:选项式API的TypeScript类型推断较差。
- 打包体积:选项式API不支持Tree Shaking,打包体积较大。
2. 组合式API与选项式API的区别是什么?
| 特性 | 组合式API | 选项式API |
|---|---|---|
| 代码组织 | 按逻辑关注点组织 | 按选项类型组织 |
| 代码复用 | 支持逻辑复用,可提取为单独的函数 | 支持mixins,但可能导致命名冲突 |
| 类型推断 | 更好的TypeScript类型推断 | 类型推断较差 |
| 打包体积 | 支持Tree Shaking,打包体积更小 | 不支持Tree Shaking,打包体积较大 |
| 学习曲线 | 学习曲线较陡 | 学习曲线较平缓 |
3. setup函数的作用是什么?它在组件生命周期中的位置是什么?
setup函数是组合式API的入口点,它在组件创建之前执行,替代了Vue 2中的beforeCreate和created钩子。
作用:
- 初始化响应式数据。
- 定义方法和计算属性。
- 注册生命周期钩子。
- 返回需要暴露给模板的变量和方法。
4. 组合式API提供了哪些响应式API?
组合式API提供了以下响应式API:
- ref:用于创建响应式的基本类型数据。
- reactive:用于创建响应式的对象类型数据。
- toRefs:用于将响应式对象转换为ref对象的集合。
- computed:用于创建计算属性。
- watch:用于监听响应式数据的变化。
- watchEffect:用于自动追踪响应式依赖的变化。
5. ref和reactive的区别是什么?
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型和对象类型 | 仅对象类型 |
| 访问方式 | 需要使用.value访问和修改 | 直接访问和修改属性 |
| 模板使用 | 自动解包,不需要.value | 直接使用 |
| 解构行为 | 解构后仍保持响应性 | 解构后失去响应性,需要使用toRefs |
6. watch和watchEffect的区别是什么?
| 特性 | watch | watchEffect |
|---|---|---|
| 执行时机 | 首次不会执行,只有当监听的数据变化时才执行 | 首次会立即执行,然后当依赖变化时执行 |
| 依赖追踪 | 需要明确指定监听的数据 | 自动追踪回调函数中使用的响应式数据 |
| 新旧值 | 可以获取新旧值 | 无法获取新旧值 |
| 适用场景 | 需要明确知道数据变化前后值的场景 | 需要自动追踪依赖的场景 |
7. 如何在组合式API中使用生命周期钩子?
在组合式API中,生命周期钩子需要从vue中导入,然后在setup函数中使用:
import { onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('Component mounted');
});
onBeforeUnmount(() => {
console.log('Component before unmount');
});
return {};
}
};8. 如何在组合式API中实现依赖注入?
在组合式API中,依赖注入需要使用provide和inject函数:
// 父组件
import { provide, ref } from 'vue';
export default {
setup() {
const message = ref('Hello from parent');
provide('message', message);
return {};
}
};
// 子组件
import { inject } from 'vue';
export default {
setup() {
const message = inject('message');
return {
message
};
}
};9. 组合式API的优势是什么?
组合式API的优势:
- 更好的代码组织:按逻辑关注点组织代码,使代码更加清晰易读。
- 更好的代码复用:支持逻辑复用,可提取为单独的函数。
- 更好的类型推断:与TypeScript配合使用可以获得更好的类型推断支持。
- 更小的打包体积:支持Tree Shaking,打包体积更小。
10. 组合式API的适用场景是什么?
组合式API适用于以下场景:
- 大型组件:需要按逻辑关注点组织代码的大型组件。
- 需要逻辑复用的场景:多个组件需要共享相同的逻辑。
- 使用TypeScript的场景:需要更好的TypeScript类型推断支持。
- 对打包体积有要求的场景:需要更小的打包体积。
总结
组合式API是Vue 3中引入的重要特性,它提供了一种新的组织组件逻辑的方式,使代码更加可维护和可复用。通过合理使用组合式API,可以提高代码的质量和开发效率。
组合式API与选项式API并不是互斥的,它们可以在同一个项目中混合使用。对于小型组件,选项式API可能更加直观;对于大型组件,组合式API可能更加适合。
在实际开发中,应该根据具体的场景选择合适的API风格,以充分发挥Vue的优势。