Options API 与 Composition API 对照表
学习目标
完成本章学习后,你将能够:
- 理解Options API和Composition API的核心区别
- 快速将Options API代码转换为Composition API
- 根据项目需求选择合适的API风格
- 理解两种API风格的优缺点和适用场景
前置知识
学习本章内容前,你需要掌握:
问题引入
实际场景
小李是一名前端开发者,公司的老项目使用Vue 2和Options API编写。现在公司决定将项目升级到Vue 3,并逐步迁移到Composition API。小李需要:
- 理解两种API的对应关系:如何将Options API的data、methods、computed等转换为Composition API?
- 保持功能一致性:迁移后的代码需要保持原有功能不变
- 利用新特性:在迁移过程中,如何利用Composition API的优势改进代码?
- 团队协作:如何让团队成员快速理解两种API的区别?
为什么需要这个对照表
Options API和Composition API是Vue提供的两种不同的组件编写方式:
- Options API:Vue 2的传统写法,通过配置对象(data、methods、computed等)组织代码
- Composition API:Vue 3引入的新写法,通过组合函数的方式组织代码,提供更好的逻辑复用和类型推导
这个对照表将帮助你:
- 快速找到Options API在Composition API中的对应写法
- 理解两种API风格的设计思想差异
- 顺利完成项目迁移和代码重构
- 在新项目中做出合适的技术选择
核心概念
概念1:API风格对比
Options API特点
Options API通过配置对象的方式组织代码,每个选项负责特定的功能:
export default {
data() {
return {
// 响应式数据
}
},
computed: {
// 计算属性
},
methods: {
// 方法
},
mounted() {
// 生命周期钩子
}
}
优点:
- 结构清晰,容易理解
- 适合小型组件
- 学习曲线平缓
缺点:
- 逻辑分散在不同选项中
- 难以复用逻辑
- TypeScript支持较弱
Composition API特点
Composition API通过组合函数的方式组织代码,相关逻辑可以放在一起:
<script setup>
import { ref, computed, onMounted } from 'vue';
// 所有逻辑都在setup中,可以按功能组织
const count = ref(0);
const doubled = computed(() => count.value * 2);
onMounted(() => {
// 生命周期逻辑
});
</script>
优点:
- 逻辑复用更容易(组合式函数)
- 更好的TypeScript支持
- 更灵活的代码组织
- 更好的tree-shaking
缺点:
- 学习曲线较陡
- 需要理解ref和reactive的区别
- 代码可能不如Options API结构化
概念2:核心API对照
下面是两种API风格的完整对照表:
完整对照表
1. 响应式数据
| Options API | Composition API | 说明 |
|---|---|---|
data() | ref() / reactive() | 定义响应式数据 |
Options API示例:
<script>
export default {
data() {
return {
count: 0,
user: {
name: '张三',
age: 25
}
}
}
}
</script>
Composition API示例:
<script setup>
import { ref, reactive } from 'vue';
// 使用ref定义基本类型
const count = ref(0);
// 使用reactive定义对象
const user = reactive({
name: '张三',
age: 25
});
// 注意:访问ref需要.value,reactive不需要
console.log(count.value); // 0
console.log(user.name); // '张三'
</script>
2. 计算属性
| Options API | Composition API | 说明 |
|---|---|---|
computed | computed() | 定义计算属性 |
Options API示例:
<script>
export default {
data() {
return {
firstName: '张',
lastName: '三'
}
},
computed: {
// 只读计算属性
fullName() {
return this.firstName + this.lastName;
},
// 可写计算属性
reversedName: {
get() {
return this.lastName + this.firstName;
},
set(value) {
this.lastName = value[0];
this.firstName = value.slice(1);
}
}
}
}
</script>
Composition API示例:
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('张');
const lastName = ref('三');
// 只读计算属性
const fullName = computed(() => {
return firstName.value + lastName.value;
});
// 可写计算属性
const reversedName = computed({
get() {
return lastName.value + firstName.value;
},
set(value) {
lastName.value = value[0];
firstName.value = value.slice(1);
}
});
</script>
3. 方法
| Options API | Composition API | 说明 |
|---|---|---|
methods | 普通函数 | 定义方法 |
Options API示例:
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
reset() {
this.count = 0;
}
}
}
</script>
Composition API示例:
<script setup>
import { ref } from 'vue';
const count = ref(0);
// 直接定义函数,不需要methods选项
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = 0;
};
</script>
4. 侦听器
| Options API | Composition API | 说明 |
|---|---|---|
watch | watch() / watchEffect() | 侦听数据变化 |
Options API示例:
<script>
export default {
data() {
return {
question: '',
answer: '请输入问题'
}
},
watch: {
// 简单侦听
question(newValue, oldValue) {
console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
this.getAnswer();
},
// 深度侦听
user: {
handler(newValue, oldValue) {
console.log('用户信息变化');
},
deep: true,
immediate: true
}
},
methods: {
getAnswer() {
this.answer = '正在思考...';
}
}
}
</script>
Composition API示例:
<script setup>
import { ref, watch, watchEffect } from 'vue';
const question = ref('');
const answer = ref('请输入问题');
const user = ref({ name: '张三', age: 25 });
// 使用watch侦听特定数据源
watch(question, (newValue, oldValue) => {
console.log(`问题从 "${oldValue}" 变为 "${newValue}"`);
getAnswer();
});
// 深度侦听对象
watch(user, (newValue, oldValue) => {
console.log('用户信息变化');
}, {
deep: true,
immediate: true
});
// 使用watchEffect自动追踪依赖
watchEffect(() => {
// 自动追踪question的变化
console.log(`当前问题:${question.value}`);
});
const getAnswer = () => {
answer.value = '正在思考...';
};
</script>
5. 生命周期钩子
| Options API | Composition API | 说明 |
|---|---|---|
beforeCreate | - | 使用setup()替代 |
created | - | 使用setup()替代 |
beforeMount | onBeforeMount() | 挂载前 |
mounted | onMounted() | 挂载后 |
beforeUpdate | onBeforeUpdate() | 更新前 |
updated | onUpdated() | 更新后 |
beforeUnmount | onBeforeUnmount() | 卸载前 |
unmounted | onUnmounted() | 卸载后 |
errorCaptured | onErrorCaptured() | 错误捕获 |
activated | onActivated() | keep-alive激活 |
deactivated | onDeactivated() | keep-alive停用 |
Options API示例:
<script>
export default {
data() {
return {
message: 'Hello'
}
},
beforeCreate() {
console.log('beforeCreate: 实例初始化之后');
},
created() {
console.log('created: 实例创建完成');
// 可以访问this.message
},
beforeMount() {
console.log('beforeMount: 挂载开始之前');
},
mounted() {
console.log('mounted: 挂载完成');
// 可以访问DOM
},
beforeUpdate() {
console.log('beforeUpdate: 数据更新前');
},
updated() {
console.log('updated: 数据更新后');
},
beforeUnmount() {
console.log('beforeUnmount: 卸载前');
},
unmounted() {
console.log('unmounted: 卸载完成');
// 清理定时器、事件监听等
}
}
</script>
Composition API示例:
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
const message = ref('Hello');
// setup()本身就相当于beforeCreate和created
console.log('setup执行,相当于created');
onBeforeMount(() => {
console.log('onBeforeMount: 挂载开始之前');
});
onMounted(() => {
console.log('onMounted: 挂载完成');
// 可以访问DOM
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate: 数据更新前');
});
onUpdated(() => {
console.log('onUpdated: 数据更新后');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount: 卸载前');
});
onUnmounted(() => {
console.log('onUnmounted: 卸载完成');
// 清理定时器、事件监听等
});
</script>
6. Props
| Options API | Composition API | 说明 |
|---|---|---|
props | defineProps() | 定义组件属性 |
Options API示例:
<script>
export default {
props: {
// 简单声明
title: String,
// 详细声明
count: {
type: Number,
required: true,
default: 0,
validator(value) {
return value >= 0;
}
},
user: {
type: Object,
default: () => ({ name: '匿名' })
}
},
mounted() {
// 通过this访问props
console.log(this.title);
console.log(this.count);
}
}
</script>
Composition API示例:
<script setup>
import { computed } from 'vue';
// 简单声明
// const props = defineProps(['title', 'count']);
// 详细声明(推荐)
const props = defineProps({
title: String,
count: {
type: Number,
required: true,
default: 0,
validator(value) {
return value >= 0;
}
},
user: {
type: Object,
default: () => ({ name: '匿名' })
}
});
// TypeScript类型声明(更推荐)
// const props = defineProps<{
// title?: string;
// count: number;
// user?: { name: string };
// }>();
// 直接访问props,不需要this
console.log(props.title);
console.log(props.count);
// props是响应式的,可以在computed中使用
const doubledCount = computed(() => props.count * 2);
</script>
7. Emits(事件)
| Options API | Composition API | 说明 |
|---|---|---|
emits + $emit | defineEmits() | 定义和触发事件 |
Options API示例:
<script>
export default {
emits: ['update', 'delete'],
// 或者详细声明
emits: {
update: (value) => {
// 验证事件参数
return typeof value === 'string';
},
delete: null
},
methods: {
handleClick() {
// 触发事件
this.$emit('update', 'new value');
},
handleDelete() {
this.$emit('delete');
}
}
}
</script>
Composition API示例:
<script setup>
// 简单声明
// const emit = defineEmits(['update', 'delete']);
// 详细声明(推荐)
const emit = defineEmits({
update: (value) => {
// 验证事件参数
return typeof value === 'string';
},
delete: null
});
// TypeScript类型声明(更推荐)
// const emit = defineEmits<{
// update: [value: string];
// delete: [];
// }>();
const handleClick = () => {
// 触发事件
emit('update', 'new value');
};
const handleDelete = () => {
emit('delete');
};
</script>
8. 插槽
| Options API | Composition API | 说明 |
|---|---|---|
$slots | useSlots() | 访问插槽 |
Options API示例:
<script>
export default {
mounted() {
// 检查插槽是否存在
if (this.$slots.default) {
console.log('有默认插槽内容');
}
if (this.$slots.header) {
console.log('有header插槽内容');
}
}
}
</script>
<template>
<div>
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>
</template>
Composition API示例:
<script setup>
import { useSlots, onMounted } from 'vue';
// 获取插槽对象
const slots = useSlots();
onMounted(() => {
// 检查插槽是否存在
if (slots.default) {
console.log('有默认插槽内容');
}
if (slots.header) {
console.log('有header插槽内容');
}
});
</script>
<template>
<div>
<header v-if="slots.header">
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>
</template>
9. Refs(模板引用)
| Options API | Composition API | 说明 |
|---|---|---|
$refs | ref() | 访问DOM或组件实例 |
Options API示例:
<script>
export default {
mounted() {
// 访问DOM元素
console.log(this.$refs.input);
this.$refs.input.focus();
// 访问子组件实例
console.log(this.$refs.child);
this.$refs.child.someMethod();
}
}
</script>
<template>
<div>
<input ref="input" />
<ChildComponent ref="child" />
</div>
</template>
Composition API示例:
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 创建ref,变量名必须与模板中的ref属性值相同
const input = ref(null);
const child = ref(null);
onMounted(() => {
// 访问DOM元素
console.log(input.value);
input.value.focus();
// 访问子组件实例
console.log(child.value);
child.value.someMethod();
});
</script>
<template>
<div>
<input ref="input" />
<ChildComponent ref="child" />
</div>
</template>
10. 暴露组件方法
| Options API | Composition API | 说明 |
|---|---|---|
| 自动暴露 | defineExpose() | 暴露组件内部方法给父组件 |
Options API示例:
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
},
getCount() {
return this.count;
}
}
// Options API中,所有methods和data都会自动暴露给父组件
}
</script>
Composition API示例:
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
const getCount = () => {
return count.value;
};
// Composition API中,默认不暴露任何内容
// 需要使用defineExpose显式暴露
defineExpose({
increment,
getCount
// 注意:通常不暴露响应式数据本身
});
</script>
11. Provide / Inject(依赖注入)
| Options API | Composition API | 说明 |
|---|---|---|
provide / inject | provide() / inject() | 跨层级组件通信 |
Options API示例:
<!-- 祖先组件 -->
<script>
export default {
data() {
return {
theme: 'dark'
}
},
provide() {
return {
theme: this.theme,
// 注意:这样提供的值不是响应式的
updateTheme: this.updateTheme
}
},
methods: {
updateTheme(newTheme) {
this.theme = newTheme;
}
}
}
</script>
<!-- 后代组件 -->
<script>
export default {
inject: ['theme', 'updateTheme'],
mounted() {
console.log(this.theme); // 'dark'
this.updateTheme('light');
}
}
</script>
Composition API示例:
<!-- 祖先组件 -->
<script setup>
import { ref, provide } from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme) => {
theme.value = newTheme;
};
// 提供响应式数据
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>
<!-- 后代组件 -->
<script setup>
import { inject, onMounted } from 'vue';
// 注入数据,可以提供默认值
const theme = inject('theme', 'light');
const updateTheme = inject('updateTheme');
onMounted(() => {
console.log(theme.value); // 'dark'
updateTheme('light');
});
</script>
12. Mixins(混入)
| Options API | Composition API | 说明 |
|---|---|---|
mixins | 组合式函数 | 逻辑复用 |
Options API示例:
// mixins/logger.js
export const loggerMixin = {
data() {
return {
logCount: 0
}
},
methods: {
log(message) {
console.log(message);
this.logCount++;
}
},
mounted() {
console.log('Logger mixin mounted');
}
};
// 使用mixin
export default {
mixins: [loggerMixin],
mounted() {
this.log('组件已挂载');
console.log(`日志次数:${this.logCount}`);
}
}
Composition API示例:
// composables/useLogger.js
import { ref, onMounted } from 'vue';
export function useLogger() {
const logCount = ref(0);
const log = (message) => {
console.log(message);
logCount.value++;
};
onMounted(() => {
console.log('Logger composable mounted');
});
return {
logCount,
log
};
}
// 使用组合式函数
import { useLogger } from './composables/useLogger';
const { logCount, log } = useLogger();
onMounted(() => {
log('组件已挂载');
console.log(`日志次数:${logCount.value}`);
});
最佳实践
企业级应用场景
场景1:大型组件迁移策略
在企业级项目中,通常不会一次性将所有组件从Options API迁移到Composition API。推荐采用渐进式迁移策略:
<!-- 步骤1:保持Options API,先熟悉Composition API -->
<script>
export default {
// 保持原有Options API代码
data() {
return {
count: 0,
message: 'Hello'
}
},
methods: {
increment() {
this.count++;
}
}
}
</script>
<!-- 步骤2:混合使用,逐步迁移 -->
<script>
import { ref, computed } from 'vue';
export default {
// 新功能使用Composition API
setup() {
const newFeature = ref('');
const processedFeature = computed(() => newFeature.value.toUpperCase());
return {
newFeature,
processedFeature
};
},
// 旧功能保持Options API
data() {
return {
count: 0,
message: 'Hello'
}
},
methods: {
increment() {
this.count++;
}
}
}
</script>
<!-- 步骤3:完全迁移到Composition API -->
<script setup>
import { ref, computed } from 'vue';
// 所有逻辑都使用Composition API
const count = ref(0);
const message = ref('Hello');
const newFeature = ref('');
const processedFeature = computed(() => newFeature.value.toUpperCase());
const increment = () => {
count.value++;
};
</script>
迁移建议:
- 从小型、独立的组件开始迁移
- 优先迁移新功能和新组件
- 对于复杂组件,可以先混合使用
- 充分测试迁移后的功能
- 团队成员需要先学习Composition API基础
场景2:逻辑复用最佳实践
Composition API的最大优势是逻辑复用。下面是一个完整的企业级示例:
// composables/useUserManagement.js
/**
* 用户管理组合式函数
* 封装用户相关的所有逻辑,包括获取、更新、删除等操作
*/
import { ref, computed, onMounted } from 'vue';
import { userApi } from '@/api/user';
export function useUserManagement() {
// 响应式状态
const users = ref([]);
const loading = ref(false);
const error = ref(null);
const currentPage = ref(1);
const pageSize = ref(10);
// 计算属性
const totalPages = computed(() => {
return Math.ceil(users.value.length / pageSize.value);
});
const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return users.value.slice(start, end);
});
// 方法
const fetchUsers = async () => {
loading.value = true;
error.value = null;
try {
const response = await userApi.getUsers();
users.value = response.data;
} catch (err) {
error.value = err.message;
console.error('获取用户列表失败:', err);
} finally {
loading.value = false;
}
};
const deleteUser = async (userId) => {
try {
await userApi.deleteUser(userId);
// 从列表中移除已删除的用户
users.value = users.value.filter(user => user.id !== userId);
} catch (err) {
error.value = err.message;
throw err;
}
};
const updateUser = async (userId, userData) => {
try {
const response = await userApi.updateUser(userId, userData);
// 更新列表中的用户数据
const index = users.value.findIndex(user => user.id === userId);
if (index !== -1) {
users.value[index] = response.data;
}
} catch (err) {
error.value = err.message;
throw err;
}
};
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
}
};
// 生命周期
onMounted(() => {
fetchUsers();
});
// 返回需要暴露的状态和方法
return {
// 状态
users,
loading,
error,
currentPage,
pageSize,
// 计算属性
totalPages,
paginatedUsers,
// 方法
fetchUsers,
deleteUser,
updateUser,
goToPage
};
}
在组件中使用:
<script setup>
import { useUserManagement } from '@/composables/useUserManagement';
import { ElMessage } from 'element-plus';
// 使用组合式函数,获取所有用户管理相关的功能
const {
paginatedUsers,
loading,
error,
currentPage,
totalPages,
deleteUser,
goToPage
} = useUserManagement();
// 处理删除操作
const handleDelete = async (userId) => {
try {
await deleteUser(userId);
ElMessage.success('删除成功');
} catch (err) {
ElMessage.error('删除失败');
}
};
</script>
<template>
<div class="user-management">
<!-- 加载状态 -->
<div v-if="loading" class="loading">加载中...</div>
<!-- 错误提示 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 用户列表 -->
<div v-else class="user-list">
<div v-for="user in paginatedUsers" :key="user.id" class="user-item">
<span>{{ user.name }}</span>
<button @click="handleDelete(user.id)">删除</button>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<button
@click="goToPage(currentPage - 1)"
:disabled="currentPage === 1"
>
上一页
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button
@click="goToPage(currentPage + 1)"
:disabled="currentPage === totalPages"
>
下一页
</button>
</div>
</div>
</template>
优势分析:
- 逻辑集中:所有用户管理相关的逻辑都在一个文件中
- 易于复用:多个组件可以使用同一个组合式函数
- 易于测试:可以单独测试组合式函数
- 类型安全:配合TypeScript可以获得完整的类型提示
- 按需引入:只引入需要的功能,减少组件代码量
常见陷阱
陷阱1:忘记.value
错误示例:
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
// ❌ 错误:忘记使用.value
count++; // 这不会触发响应式更新
};
</script>
正确做法:
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
// ✅ 正确:使用.value访问和修改ref的值
count.value++;
};
</script>
原因分析:
ref()返回的是一个响应式引用对象,不是原始值- 在JavaScript中访问和修改ref需要使用
.value - 在模板中Vue会自动解包,不需要
.value
陷阱2:reactive对象的解构
错误示例:
<script setup>
import { reactive } from 'vue';
const state = reactive({
count: 0,
message: 'Hello'
});
// ❌ 错误:直接解构会失去响应性
const { count, message } = state;
const increment = () => {
count++; // 这不会触发响应式更新
};
</script>
正确做法:
<script setup>
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
message: 'Hello'
});
// ✅ 正确:使用toRefs保持响应性
const { count, message } = toRefs(state);
const increment = () => {
count.value++; // 现在可以正常工作
};
// 或者不解构,直接使用state
const increment2 = () => {
state.count++; // 这也可以正常工作
};
</script>
原因分析:
- 直接解构
reactive对象会失去响应性 toRefs()将reactive对象的每个属性转换为ref- 转换后的ref保持与原对象的响应式连接
陷阱3:watch的immediate选项
错误示例:
<script setup>
import { ref, watch } from 'vue';
const userId = ref(null);
const userData = ref(null);
// ❌ 问题:只有userId变化时才会执行
watch(userId, async (newId) => {
if (newId) {
const response = await fetchUser(newId);
userData.value = response.data;
}
});
// 如果userId初始值不是null,watch不会立即执行
// 需要手动调用一次fetchUser
</script>
正确做法:
<script setup>
import { ref, watch } from 'vue';
const userId = ref(123); // 初始值不是null
const userData = ref(null);
// ✅ 正确:使用immediate选项立即执行一次
watch(userId, async (newId) => {
if (newId) {
const response = await fetchUser(newId);
userData.value = response.data;
}
}, {
immediate: true // 组件挂载时立即执行一次
});
</script>
原因分析:
- 默认情况下,
watch只在数据变化时执行 - 使用
immediate: true可以在组件挂载时立即执行一次 - 这对于需要根据初始值加载数据的场景非常有用
性能优化建议
建议1:合理选择ref和reactive
<script setup>
import { ref, reactive } from 'vue';
// ✅ 推荐:基本类型使用ref
const count = ref(0);
const message = ref('Hello');
const isActive = ref(false);
// ✅ 推荐:对象使用reactive(如果不需要整体替换)
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
// ❌ 不推荐:对象使用ref(除非需要整体替换)
const user2 = ref({
name: '李四',
age: 30
});
// 访问属性需要user2.value.name,比较繁琐
// ✅ 但如果需要整体替换对象,ref更合适
const config = ref({ theme: 'dark' });
// 可以整体替换
config.value = { theme: 'light', fontSize: 14 };
</script>
建议2:使用computed缓存计算结果
<script setup>
import { ref, computed } from 'vue';
const items = ref([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 },
{ id: 3, name: '商品C', price: 150, quantity: 3 }
]);
// ✅ 推荐:使用computed缓存计算结果
const totalPrice = computed(() => {
console.log('计算总价'); // 只在items变化时执行
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
});
// ❌ 不推荐:使用方法每次都重新计算
const getTotalPrice = () => {
console.log('计算总价'); // 每次调用都执行
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
};
</script>
<template>
<div>
<!-- computed会缓存结果,多次使用不会重复计算 -->
<p>总价:{{ totalPrice }}</p>
<p>总价(含税):{{ totalPrice * 1.1 }}</p>
<!-- 方法每次都会重新计算 -->
<p>总价:{{ getTotalPrice() }}</p>
<p>总价(含税):{{ getTotalPrice() * 1.1 }}</p>
</div>
</template>
建议3:避免在模板中使用复杂表达式
<script setup>
import { ref, computed } from 'vue';
const users = ref([
{ id: 1, name: '张三', age: 25, status: 'active' },
{ id: 2, name: '李四', age: 30, status: 'inactive' },
{ id: 3, name: '王五', age: 28, status: 'active' }
]);
// ❌ 不推荐:在模板中使用复杂表达式
// <div v-for="user in users.filter(u => u.status === 'active').sort((a, b) => a.age - b.age)">
// ✅ 推荐:使用computed处理复杂逻辑
const activeUsers = computed(() => {
return users.value
.filter(user => user.status === 'active')
.sort((a, b) => a.age - b.age);
});
</script>
<template>
<div>
<!-- 模板更简洁,逻辑更清晰 -->
<div v-for="user in activeUsers" :key="user.id">
{{ user.name }} - {{ user.age }}岁
</div>
</div>
</template>
实践练习
练习1:Options API转Composition API(难度:简单)
需求描述
将下面的Options API组件转换为Composition API:
<script>
export default {
data() {
return {
firstName: '',
lastName: ''
}
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
methods: {
updateFirstName(value) {
this.firstName = value;
},
updateLastName(value) {
this.lastName = value;
}
},
mounted() {
console.log('组件已挂载');
}
}
</script>
<template>
<div>
<input :value="firstName" @input="updateFirstName($event.target.value)" />
<input :value="lastName" @input="updateLastName($event.target.value)" />
<p>全名:{{ fullName }}</p>
</div>
</template>
实现提示
- 使用
ref()定义响应式数据 - 使用
computed()定义计算属性 - 直接定义函数替代methods
- 使用
onMounted()替代mounted钩子 - 记得在访问ref时使用
.value
参考答案
<script setup>
import { ref, computed, onMounted } from 'vue';
// 使用ref定义响应式数据
const firstName = ref('');
const lastName = ref('');
// 使用computed定义计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 直接定义函数
const updateFirstName = (value) => {
firstName.value = value;
};
const updateLastName = (value) => {
lastName.value = value;
};
// 使用onMounted替代mounted钩子
onMounted(() => {
console.log('组件已挂载');
});
</script>
<template>
<div>
<!-- 模板部分保持不变 -->
<input :value="firstName" @input="updateFirstName($event.target.value)" />
<input :value="lastName" @input="updateLastName($event.target.value)" />
<p>全名:{{ fullName }}</p>
</div>
</template>
答案解析
- 响应式数据:使用
ref()替代data(),因为firstName和lastName是基本类型 - 计算属性:
computed()的用法与Options API类似,但需要使用.value访问ref - 方法:直接定义函数,不需要methods选项
- 生命周期:使用
onMounted()替代mounted()钩子 - 模板:模板部分不需要修改,Vue会自动解包ref
练习2:创建可复用的组合式函数(难度:中等)
需求描述
创建一个useCounter组合式函数,实现以下功能:
- 维护一个计数器状态
- 提供增加、减少、重置方法
- 提供一个计算属性显示计数器是否为偶数
- 支持设置初始值和步长
- 在两个不同的组件中使用这个组合式函数
实现提示
- 创建一个独立的文件存放组合式函数
- 使用ref定义响应式状态
- 使用computed定义计算属性
- 函数接受配置参数(初始值、步长)
- 返回需要暴露的状态和方法
参考答案
// composables/useCounter.js
/**
* 计数器组合式函数
* @param {number} initialValue - 初始值,默认为0
* @param {number} step - 步长,默认为1
* @returns {Object} 计数器状态和方法
*/
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0, step = 1) {
// 响应式状态
const count = ref(initialValue);
// 计算属性:判断是否为偶数
const isEven = computed(() => {
return count.value % 2 === 0;
});
// 方法:增加
const increment = () => {
count.value += step;
};
// 方法:减少
const decrement = () => {
count.value -= step;
};
// 方法:重置
const reset = () => {
count.value = initialValue;
};
// 方法:设置为指定值
const setValue = (value) => {
count.value = value;
};
// 返回需要暴露的内容
return {
count,
isEven,
increment,
decrement,
reset,
setValue
};
}
组件A:基础计数器
<script setup>
import { useCounter } from '@/composables/useCounter';
// 使用默认配置
const { count, isEven, increment, decrement, reset } = useCounter();
</script>
<template>
<div class="counter">
<h2>基础计数器</h2>
<p>当前值:{{ count }}</p>
<p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
<div class="buttons">
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
<button @click="increment">+1</button>
</div>
</div>
</template>
组件B:自定义步长计数器
<script setup>
import { useCounter } from '@/composables/useCounter';
// 使用自定义配置:初始值100,步长10
const { count, isEven, increment, decrement, reset, setValue } = useCounter(100, 10);
// 可以创建多个独立的计数器实例
const counter2 = useCounter(0, 5);
</script>
<template>
<div class="counter">
<h2>自定义步长计数器</h2>
<p>当前值:{{ count }}</p>
<p>是否为偶数:{{ isEven ? '是' : '否' }}</p>
<div class="buttons">
<button @click="decrement">-10</button>
<button @click="reset">重置到100</button>
<button @click="increment">+10</button>
<button @click="setValue(0)">设置为0</button>
</div>
<hr />
<h2>第二个计数器(步长5)</h2>
<p>当前值:{{ counter2.count }}</p>
<div class="buttons">
<button @click="counter2.decrement">-5</button>
<button @click="counter2.increment">+5</button>
</div>
</div>
</template>
答案解析
-
组合式函数设计:
- 接受配置参数,提供灵活性
- 封装所有相关逻辑,包括状态、计算属性和方法
- 返回对象,方便按需解构
-
逻辑复用:
- 同一个组合式函数可以在多个组件中使用
- 每次调用都创建独立的实例,互不影响
- 可以在同一个组件中创建多个实例
-
优势体现:
- 代码复用:避免重复编写相同逻辑
- 逻辑集中:所有计数器相关逻辑都在一个文件中
- 易于测试:可以单独测试组合式函数
- 类型安全:配合TypeScript可以获得完整的类型提示
-
与Mixin对比:
- 组合式函数的来源清晰(显式导入)
- 不会有命名冲突问题
- 可以传递参数配置
- 更好的TypeScript支持