引言
Vue.js 作为前端开发领域的热门框架之一,在 Vue 3 中引入了 Composition API(组合式 API),为开发者带来了全新的代码组织方式。相比传统的 Options API,Composition API 更加灵活,能够更好地应对复杂逻辑的开发需求。它通过将相关逻辑集中在一起,提升了代码的可读性和可维护性,成为现代 Vue 开发的标配。
本文将带你从零开始学习 Composition API,涵盖基础用法、进阶技巧以及大量实际开发场景。无论你是刚接触 Vue 的新手,还是希望深入掌握 Vue 3 的老手,这篇文章都能为你提供实用的知识和灵感。
一、Composition API 基础
1.1 什么是 setup
函数?
setup
函数是 Composition API 的核心,它是组件的入口点,在组件创建时被调用,用于定义响应式数据、计算属性、方法等。它接收两个参数:
props
:组件接收的外部属性。context
:包含attrs
(非 prop 属性)、slots
(插槽)和emit
(事件触发器)等。
简单示例:
<template>
<div>{{ count }} * 2 = {{ double }}</div>
<button @click="increment">加 1</button>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0); // 响应式数据
const double = computed(() => count.value * 2); // 计算属性
const increment = () => { count.value++; }; // 方法
return { count, double, increment }; // 返回给模板使用
},
};
</script>
在这个例子中,setup
函数定义了 count
(计数器)、double
(计算属性)和 increment
(加 1 方法),并通过 return
将它们暴露给模板。
1.2 响应式数据:ref
和 reactive
Vue 的响应式系统是其核心特性之一,Composition API 提供了 ref
和 reactive
来创建响应式数据。
ref
:用于基本数据类型(如数字、字符串)的响应式引用。访问和修改时需要使用.value
。reactive
:用于复杂数据类型(如对象、数组)的响应式引用,不需要.value
。
示例:
<template>
<div>{{ count }} - {{ user.name }}</div>
<button @click="count++">加 1</button>
<button @click="user.name = '李四'">改名</button>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const count = ref(0);
const user = reactive({ name: '张三', age: 18 });
return { count, user };
},
};
</script>
注意:在模板中,ref
的值会自动解包,无需写 .value
。
1.3 计算属性:computed
computed
用于创建依赖于其他响应式数据的计算属性,只有当依赖发生变化时才会重新计算。
示例:
<template>
<div>总价:{{ totalPrice }}</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const price = ref(100);
const quantity = ref(2);
const totalPrice = computed(() => price.value * quantity.value);
return { totalPrice };
},
};
</script>
1.4 侦听器:watch
和 watchEffect
watch
:显式监听某个数据源的变化,并执行回调函数。watchEffect
:自动追踪依赖的变化并执行副作用。
示例:
<template>
<div>{{ count }}</div>
<button @click="count++">加 1</button>
</template>
<script>
import { ref, watch, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
// watch:显式监听 count
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
});
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`count 当前值:${count.value}`);
});
return { count };
},
};
</script>
二、进阶用法
2.1 生命周期钩子
Composition API 提供了与 Options API 类似的生命周期钩子,如 onMounted
、onUpdated
等,在 setup
中使用。
示例:
<template>
<div>{{ message }}</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
const message = ref('Hello');
onMounted(() => {
console.log('组件已挂载');
});
onUnmounted(() => {
console.log('组件已卸载');
});
return { message };
},
};
</script>
2.2 依赖注入:provide
和 inject
provide
和 inject
用于跨组件传递数据,特别适合祖孙组件通信。
示例:
<!-- 父组件 -->
<template>
<Child />
</template>
<script>
import { provide } from 'vue';
import Child from './Child.vue';
export default {
setup() {
provide('config', { theme: 'dark' });
},
components: { Child },
};
</script>
<!-- 子组件 -->
<template>
<div>主题:{{ theme }}</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const config = inject('config');
return { theme: config.theme };
},
};
</script>
2.3 自定义 Hooks
自定义 Hooks 是 Composition API 的亮点之一,可以封装可复用的逻辑。
示例:鼠标位置追踪 Hook
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (event) => {
x.value = event.pageX;
y.value = event.pageY;
};
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return { x, y };
}
使用:
<template>
<div>鼠标位置:{{ x }}, {{ y }}</div>
</template>
<script>
import { useMouse } from './useMouse';
export default {
setup() {
const { x, y } = useMouse();
return { x, y };
},
};
</script>
三、实际开发应用场景
3.1 表单处理与验证
案例:用户注册表单
<template>
<form @submit.prevent="submit">
<input v-model="form.username" placeholder="用户名" />
<input v-model="form.password" type="password" placeholder="密码" />
<div v-if="errors.username">{{ errors.username }}</div>
<div v-if="errors.password">{{ errors.password }}</div>
<button type="submit" :disabled="hasErrors">提交</button>
</form>
</template>
<script>
import { reactive, computed } from 'vue';
export default {
setup() {
const form = reactive({
username: '',
password: '',
});
const errors = computed(() => {
const errs = {};
if (!form.username) errs.username = '用户名不能为空';
if (form.password.length < 6) errs.password = '密码至少 6 位';
return errs;
});
const hasErrors = computed(() => Object.keys(errors.value).length > 0);
const submit = () => {
if (!hasErrors.value) {
console.log('提交成功:', form);
}
};
return { form, errors, hasErrors, submit };
},
};
</script>
3.2 数据请求与状态管理
案例:获取用户列表
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const users = ref([]);
const loading = ref(true);
const error = ref(null);
const fetchUsers = async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
users.value = await res.json();
} catch (err) {
error.value = '加载失败:' + err.message;
} finally {
loading.value = false;
}
};
onMounted(fetchUsers);
return { users, loading, error };
},
};
</script>
3.3 组件通信与状态共享
案例:主题切换
<!-- App.vue -->
<template>
<div :class="theme">
<ThemeToggle />
<Child />
</div>
</template>
<script>
import { ref, provide } from 'vue';
import ThemeToggle from './ThemeToggle.vue';
import Child from './Child.vue';
export default {
setup() {
const theme = ref('light');
provide('theme', theme);
return { theme };
},
components: { ThemeToggle, Child },
};
</script>
<!-- ThemeToggle.vue -->
<template>
<button @click="toggleTheme">切换主题</button>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme');
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
return { toggleTheme };
},
};
</script>
<!-- Child.vue -->
<template>
<div>当前主题:{{ theme }}</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme');
return { theme };
},
};
</script>
3.4 动态表格与分页
案例:带分页的用户表格
<template>
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr v-for="user in paginatedUsers" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<span>第 {{ currentPage }} 页</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
const users = ref([]);
const currentPage = ref(1);
const pageSize = 5;
const totalPages = computed(() => Math.ceil(users.value.length / pageSize));
const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize;
const end = start + pageSize;
return users.value.slice(start, end);
});
const fetchUsers = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
users.value = await res.json();
};
const prevPage = () => { if (currentPage.value > 1) currentPage.value--; };
const nextPage = () => { if (currentPage.value < totalPages.value) currentPage.value++; };
onMounted(fetchUsers);
return { paginatedUsers, currentPage, totalPages, prevPage, nextPage };
},
};
</script>
四、优化技巧
4.1 性能优化
- 减少不必要计算:使用
computed
缓存计算结果,避免重复计算。 - 控制
watchEffect
:尽量减少依赖项,防止频繁触发。 - 解构响应式对象:使用
toRefs
保持响应性。
示例:
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Vue' });
const { count, name } = toRefs(state); // 解构后仍保持响应性
4.2 代码组织与重用
- 抽离逻辑:将复杂逻辑封装为自定义 Hooks。
- 模块化:将功能按模块拆分到单独文件中。
示例:计数器 Hook
// useCounter.js
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => { count.value++; };
const decrement = () => { count.value--; };
return { count, increment, decrement };
}
使用:
<template>
<div>{{ count }}</div>
<button @click="increment">加 1</button>
<button @click="decrement">减 1</button>
</template>
<script>
import { useCounter } from './useCounter';
export default {
setup() {
const { count, increment, decrement } = useCounter(10);
return { count, increment, decrement };
},
};
</script>
4.3 错误处理
在异步操作中添加完善的错误处理逻辑。
示例:
<template>
<div v-if="error">{{ error }}</div>
<div v-else>{{ data }}</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const data = ref(null);
const error = ref(null);
const fetchData = async () => {
try {
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error('网络错误');
data.value = await res.json();
} catch (err) {
error.value = err.message;
}
};
onMounted(fetchData);
return { data, error };
},
};
</script>
五、未来趋势展望
随着 Vue 3 的普及,Composition API 将成为 Vue 开发的主流方式。它与 TypeScript 的深度结合将为大型项目提供更好的类型支持。此外,社区可能会推出更多基于 Composition API 的插件和工具,如状态管理库、表单处理库等,进一步丰富 Vue 生态。
未来,Composition API 还可能与 Vite 等现代构建工具深度整合,提升开发体验和性能。
六、总结
Composition API 是 Vue 3 的核心特性之一,它不仅提升了代码的灵活性,还为开发者提供了强大的工具来应对复杂场景。通过本文的学习,你应该能够掌握从基础到进阶的用法,并在实际开发中灵活运用。
希望你能在实践中不断探索 Composition API 的潜力,构建出高效、可维护的前端应用!
这篇文章详细介绍了 Composition API 的方方面面,包含多个实用案例和优化建议,适合不同层次的开发者学习和参考。希望对你有所帮助!