组合式API | 自在学组合式API
在前面的课程里,我们已经多次使用了组合式API。组合式API是Vue 3的重要特性,它提供了更好的逻辑复用和代码组织方式。这一节课,我们将深入学习组合式API的高级特性,包括setup函数的深入理解、组合式函数(composables)的创建和使用、响应式工具函数等。
掌握组合式API的高级特性,可以帮助我们写出更加模块化、可复用和易维护的代码。这些知识是构建大型Vue应用的基础。

setup函数
setup函数是组合式API的核心,在每个组件实例被创建之前执行。该函数会被Vue在初始化组件时调用,并允许你用组合式API来构建组件的逻辑。在setup中,我们可以访问两个参数:
props:组件传入的属性对象,是响应式的,可以直接通过props.xxx访问父组件传递进来的数据。注意直接解构props会丢失响应性,如果确实需要分解,可以使用Vue的toRefs工具。
context:上下文对象,包含了attrs(除props外传入的属性和事件监听器)、slots(插槽内容)、emit(自定义事件触发方法)等。这些可以帮助我们在setup中与外部进行交互。
<div id="app">
<user-card name="张三" :age="25"></user-card>
</div>
<script>
const { createApp } = Vue;
const UserCard = {
props: {
name: String,
age: Number
},
setup(props) {
// props是响应式的,但不要解构它
console.log('name:', props.name);
console.log('age:', props.age);
// 如果需要解构,使用toRefs
const { toRefs } = Vue;
const { name, age } = toRefs(props);
return {
name,
age
};
},
template: `
<div>
<h3>{{ name }}</h3>
<p>年龄:{{ age }}</p>
</div>
`
};
const app = createApp({});
app.component('user-card', UserCard);
app.mount('#app');
</script>
在这个例子中,setup函数接收props参数。注意,props是响应式的,但如果我们直接解构它,会失去响应性。如果需要解构,应该使用toRefs。
context参数包含了组件的上下文信息:
<div id="app">
<child-component @custom-event="handleEvent"></child-component>
</div>
<script>
const { createApp } = Vue;
const ChildComponent = {
setup(props, context) {
// context.attrs: 包含所有非props的属性
console.
context对象包含四个属性:attrs、slots、emit和expose。我们可以使用解构来获取这些属性,例如:
<script>
const ChildComponent = {
setup(props, { attrs, slots, emit, expose }) {
// 使用解构的方式
emit('custom-event', '数据');
return {};
}
};
</script>
组合式函数(Composables)
组合式函数(Composables)是指利用 Vue 3 组合式 API(如 ref、reactive、computed、watch 等)编写的可复用函数。它们能够将组件内部可复用的状态逻辑进行封装和抽离,方便在多个组件中共享和复用。
组合式函数的命名通常以 use 开头,例如 useCounter、useFetch 等。开发者可以在组合式函数中封装数据状态、方法、计算属性甚至副作用逻辑,然后通过 return 暴露需要在组件中使用的内容,从而提升逻辑复用性和代码组织性。
让我们创建一个简单的组合式函数:
<div id="app">
<counter-component></counter-component>
<counter-component></counter-component>
</div>
<script>
const { createApp, ref } = Vue;
// 创建一个组合式函数
function useCounter(initialValue = 0) {
const
在这个例子中,我们创建了一个useCounter组合式函数,它封装了计数器的逻辑。多个组件可以复用这个函数,每个组件都有自己独立的计数器状态。
异步组合式函数
有时候,我们需要在组合式函数中处理异步操作(比如调用接口获取数据)。此时,可以将组合式函数写为异步函数,并在其中封装 API 请求的逻辑。这样设计后,多个组件可以方便地复用同一套异步数据获取与状态管理流程。例如:
<div id="app">
<user-profile></user-profile>
</div>
<script>
const { createApp, ref } = Vue;
// 异步组合式函数
async function useUser(userId) {
const user = ref(null);
在这个例子中,我们创建了一个异步的useUser组合式函数,它用于获取用户数据。注意,setup函数也可以是异步的。
响应式工具函数
Vue提供了一些工具函数来处理响应式数据。让我们看看常用的工具函数:
toRef和toRefs
toRef和toRefs是Vue 3中用于与响应式对象密切配合的重要工具函数。它们的作用是将响应式对象(如由reactive创建的对象)中的属性“包装”为ref响应式引用,从而可以单独使用这些属性并保持响应性。
toRef(obj, key):将响应式对象obj的某个属性key转换为一个ref,这样就可以独立地对这个属性进行响应式追踪。如果直接解构reactive对象,属性会失去响应性,而使用toRef可以避免这个问题。
toRefs(obj):将整个响应式对象的每个属性都一次性转换为ref,返回的是一个包含所有ref属性的新对象。这样可以使用对象解构赋值,且不会丢失响应性。
这两个函数主要用于在解构reactive对象、或将部分属性单独传递到子组件时,确保属性依然具有响应式更新能力。
<div id="app">
<component-a></component-a>
</div>
<script>
const { createApp, reactive, toRef, toRefs } = Vue;
const ComponentA = {
setup() {
const state = reactive
unref和isRef
unref 和 isRef 是 Vue 3 Composition API 中常用的工具函数,用于更灵活地处理响应式数据。
-
unref(value):用于安全地获取ref的“真实”值。如果传入的是一个ref对象,它会返回其.value属性;如果传入的本身不是ref(比如普通的变量或响应式对象属性),则直接返回该值本身。因此,可以统一处理“可能是ref也可能不是ref”的场景,无需判断类型。例如:
unref(refVar) 返回的是 refVar.value
unref(普通变量) 返回的是该变量的值
-
isRef(value):用于判断某个变量是否为ref对象(即由ref()或computed()等API创建的响应式引用)。返回布尔值,只有传入的确实是ref时才为true,否则为false。
这两个函数可以让你在写通用组件或工具函数时,更加方便地处理混杂ref与普通变量的情况。
<div id="app">
<component-b></component-b>
</div>
<script>
const { createApp, ref, unref, isRef } = Vue;
const ComponentB = {
setup() {
const count = ref(
readonly
readonly 是 Vue 提供的一个响应式 API,用于基于现有响应式对象创建一个“只读版本”。通过 readonly 包裹之后,返回的新对象本身依然具备响应性(可以在模板或其他代码中使用),但它的属性不允许被修改,任何试图通过只读对象修改属性的操作都会失效,在开发模式下还会报出警告。
这通常用于防止数据被意外更改,尤其是在需要将响应式数据安全地传递到子组件、插件或外部函数时。例如,如果你将一个响应式对象通过 readonly 包裹后传递给子组件,子组件只能读取其内容,而无法修改数据本身,从而保证了数据的不可变性和代码的稳定。
<div id="app">
<component-c></component-c>
</div>
<script>
const { createApp, reactive, readonly } = Vue;
const ComponentC = {
setup() {
const state = reactive({
count:
创建一个完整的组合式函数
接下来,我们将详细创建一个完整的组合式函数(Composable),用于管理待办事项(Todo List),涵盖添加、删除、切换完成状态、清除已完成、以及统计数据等常见功能。这个函数将能帮助你在任意组件中轻松复用待办事项的核心逻辑,并保持状态响应式与解耦。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组合式API深入示例</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
在这个例子中,我们创建了一个完整的useTodos组合式函数,它封装了所有待办事项相关的逻辑。这个函数可以在多个组件中复用,每个组件都有自己独立的待办事项列表。
下一步
在这一节中,我们深入学习了组合式API的高级特性。我们了解了setup函数的深入用法,学习了如何创建和使用组合式函数,以及如何使用响应式工具函数。
组合式API提供了更好的逻辑复用和代码组织方式。通过组合式函数,我们可以将逻辑封装成可复用的单元,让代码更加模块化和易维护。
在下一个部分中,我们将学习Vue Router,了解如何在Vue应用中实现路由功能。这将是我们构建单页应用(SPA)必不可少的一部分。
log
(
'attrs:'
, context.attrs);
// context.slots: 包含所有插槽
console.log('slots:', context.slots);
// context.emit: 用于触发事件
const handleClick = () => {
context.emit('custom-event', '来自子组件的数据');
};
// context.expose: 用于暴露组件实例的方法或属性
context.expose({
publicMethod() {
console.log('这是公开方法');
}
});
return {
handleClick
};
},
template: `
<button @click="handleClick">触发事件</button>
`
};
const app = createApp({
setup() {
const handleEvent = (data) => {
console.log('接收到事件:', data);
};
return {
handleEvent
};
},
components: {
'child-component': ChildComponent
}
});
app.mount('#app');
</script>
count
=
ref
(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return {
count,
increment,
decrement,
reset
};
}
const CounterComponent = {
setup() {
// 使用组合式函数
const { count, increment, decrement, reset } = useCounter(10);
return {
count,
increment,
decrement,
reset
};
},
template: `
<div>
<p>计数:{{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
`
};
const app = createApp({});
app.component('counter-component', CounterComponent);
app.mount('#app');
</script>
const
loading
=
ref
(
false
);
const error = ref(null);
const fetchUser = async () => {
loading.value = true;
error.value = null;
try {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 1000));
user.value = {
id: userId,
name: '张三',
age: 25,
email: 'zhangsan@example.com'
};
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
await fetchUser();
return {
user,
loading,
error,
refetch: fetchUser
};
}
const UserProfile = {
async setup() {
const { user, loading, error, refetch } = await useUser(1);
return {
user,
loading,
error,
refetch
};
},
template: `
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误:{{ error }}</div>
<div v-else>
<h3>{{ user.name }}</h3>
<p>年龄:{{ user.age }}</p>
<p>邮箱:{{ user.email }}</p>
<button @click="refetch">刷新</button>
</div>
</div>
`
};
const app = createApp({});
app.component('user-profile', UserProfile);
app.mount('#app');
</script>
({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
// 使用toRef转换单个属性
const nameRef = toRef(state, 'name');
// 使用toRefs转换所有属性
const { age, email } = toRefs(state);
// 现在nameRef、age、email都是ref,可以单独使用
return {
name: nameRef,
age,
email
};
},
template: `
<div>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>邮箱:{{ email }}</p>
</div>
`
};
const app = createApp({});
app.component('component-a', ComponentA);
app.mount('#app');
</script>
0
);
const message = 'Hello';
// 使用unref获取值
console.log('count的值:', unref(count)); // 0
console.log('message的值:', unref(message)); // 'Hello'
// 使用isRef检查
console.log('count是ref吗:', isRef(count)); // true
console.log('message是ref吗:', isRef(message)); // false
return {
count
};
},
template: `
<div>
<p>计数:{{ count }}</p>
</div>
`
};
const app = createApp({});
app.component('component-b', ComponentB);
app.mount('#app');
</script>
0
});
// 创建只读版本
const readonlyState = readonly(state);
// 可以修改原始对象
state.count++;
// 尝试修改只读对象会失败(在开发模式下会警告)
// readonlyState.count++; // 这不会工作
return {
state,
readonlyState
};
},
template: `
<div>
<p>原始计数:{{ state.count }}</p>
<p>只读计数:{{ readonlyState.count }}</p>
</div>
`
};
const app = createApp({});
app.component('component-c', ComponentC);
app.mount('#app');
</script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
form {
margin-bottom: 20px;
}
input {
padding: 8px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 10px;
margin: 5px 0;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.completed {
text-decoration: line-through;
color: #999;
}
</style>
</head>
<body>
<div id="app">
<todo-manager></todo-manager>
</div>
<script>
const { createApp, ref, computed } = Vue;
function useTodos() {
const todos = ref([]);
const addTodo = (text) => {
if (text.trim()) {
todos.value.push({
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date()
});
}
};
const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id);
if (index > -1) {
todos.value.splice(index, 1);
}
};
const toggleTodo = (id) => {
const todo = todos.value.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
};
const clearCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed);
};
const totalTodos = computed(() => todos.value.length);
const completedTodos = computed(() => {
return todos.value.filter(todo => todo.completed).length;
});
const activeTodos = computed(() => {
return todos.value.filter(todo => !todo.completed).length;
});
return {
todos,
addTodo,
removeTodo,
toggleTodo,
clearCompleted,
totalTodos,
completedTodos,
activeTodos
};
}
const TodoManager = {
setup() {
const newTodo = ref('');
const {
todos,
addTodo,
removeTodo,
toggleTodo,
clearCompleted,
totalTodos,
completedTodos,
activeTodos
} = useTodos();
const handleAdd = () => {
addTodo(newTodo.value);
newTodo.value = '';
};
return {
newTodo,
todos,
handleAdd,
removeTodo,
toggleTodo,
clearCompleted,
totalTodos,
completedTodos,
activeTodos
};
},
template: `
<div>
<h2>待办事项管理</h2>
<form @submit.prevent="handleAdd">
<input v-model="newTodo" placeholder="输入待办事项">
<button type="submit">添加</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
>
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<div>
<p>总计:{{ totalTodos }} 项</p>
<p>已完成:{{ completedTodos }} 项</p>
<p>未完成:{{ activeTodos }} 项</p>
<button @click="clearCompleted">清除已完成</button>
</div>
</div>
`
};
const app = createApp({});
app.component('todo-manager', TodoManager);
app.mount('#app');
</script>
</body>
</html>