组合式 API
Vue3组合式API核心用法详解
前置知识
- Teleport 与 Suspense:建议先完成前一篇的学习
学习目标
- 掌握「1. 组合式 API 概述 | Composition API Overview」的核心机制、典型用法与常见陷阱
- 掌握「2. setup 函数 | Setup Function」的核心机制、典型用法与常见陷阱
- 掌握「3. 响应式 API | Reactive APIs」的核心机制、典型用法与常见陷阱
- 掌握「4. 生命周期钩子 | Lifecycle Hooks」的核心机制、典型用法与常见陷阱
- 掌握「5. 组合函数 | Composables」的核心机制、典型用法与常见陷阱
1. 组合式 API 概述 | Composition API Overview
组合式 API 是 Vue3 引入的新特性,它提供了一种新的方式来组织组件逻辑,使代码更易于维护和复用。与选项式 API(Options API)相比,组合式 API 具有以下优势:
- 更好的代码组织:可以将相关的逻辑组合在一起,而不是分散在不同的选项中
- 更好的类型推导:TypeScript 类型推断更加准确
- 更好的逻辑复用:可以通过组合函数(Composables)来复用逻辑
- 更灵活的代码结构:不再受选项式 API 的限制
2. setup 函数 | Setup Function
setup 函数是组合式 API 的入口点,它在组件创建之前执行,返回的对象会暴露给模板和其他选项。
2.1 基本用法
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
// 创建响应式数据
const count = ref(0);
// 定义方法
const increment = () => {
count.value++;
};
// 生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
// 返回暴露给模板的内容
return {
count,
increment,
};
},
};
</script>
2.2 script setup 语法糖
Vue3.2+ 提供了 script setup 语法糖,使组合式 API 的使用更加简洁:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 直接定义响应式数据
const count = ref(0);
// 直接定义方法
const increment = () => {
count.value++;
};
// 直接使用生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
</script>
3. 响应式 API | Reactive APIs
3.1 ref
ref 用于创建响应式的基本类型数据:
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
3.2 reactive
reactive 用于创建响应式的对象:
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
}
console.log(state.count) // 0
state.count++
console.log(state.count) // 1
3.3 computed
computed 用于创建计算属性:
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
console.log(doubleCount.value); // 0
count.value++;
console.log(doubleCount.value); // 2
3.4 watch
watch 用于监听数据变化:
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
count.value++; // 输出: Count changed from 0 to 1
3.5 watchEffect
watchEffect 用于自动追踪响应式依赖:
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`Count is ${count.value}`);
});
count.value++; // 输出: Count is 1
4. 生命周期钩子 | Lifecycle Hooks
组合式 API 提供了与选项式 API 对应的生命周期钩子:
onMounted:组件挂载后onUpdated:组件更新后onUnmounted:组件卸载后onBeforeMount:组件挂载前onBeforeUpdate:组件更新前onBeforeUnmount:组件卸载前onErrorCaptured:捕获子组件错误onRenderTracked:响应式依赖被追踪时onRenderTriggered:响应式依赖被触发时
import { onMounted, onUpdated, onUnmounted } from 'vue';
onMounted(() => {
console.log('Component mounted');
});
onUpdated(() => {
console.log('Component updated');
});
onUnmounted(() => {
console.log('Component unmounted');
});
5. 组合函数 | Composables
组合函数是组合式 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,
};
}
使用组合函数:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup>
import { useCounter } from './composables/useCounter';
const { count, doubleCount, increment, decrement } = useCounter(0);
</script>
6. 依赖注入 | Dependency Injection
6.1 provide
provide 用于向子组件提供数据:
<!-- ParentComponent.vue -->
<script setup>
import { provide, ref } from 'vue';
const message = ref('Hello from parent');
provide('message', message);
</script>
6.2 inject
inject 用于从父组件获取数据:
<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue';
const message = inject('message', 'Default message');
</script>
<template>
<p>{{ message }}</p>
</template>
7. 模板引用 | Template Refs
使用 ref 可以获取DOM元素或组件实例:
<template>
<div ref="container">Hello</div>
<MyComponent ref="myComponent" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import MyComponent from './MyComponent.vue';
const container = ref(null);
const myComponent = ref(null);
onMounted(() => {
console.log(container.value); // DOM元素
console.log(myComponent.value); // 组件实例
});
</script>
8. 响应式工具 | Reactive Utilities
8.1 toRefs
toRefs 用于将响应式对象转换为普通对象,其中每个属性都是一个 ref:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
}
const refs = toRefs(state)
console.log(refs.count.value) // 0
console.log(refs.message.value) // Hello
8.2 toRef
toRef 用于为响应式对象的单个属性创建 ref:
import { reactive, toRef } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
}
const countRef = toRef(state, 'count')
console.log(countRef.value) // 0
8.3 unref
unref 用于获取 ref 的值,如果参数不是 ref,则直接返回参数:
import { ref, unref } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(unref(count)); // 0
console.log(unref(message)); // Hello
8.4 isRef
isRef 用于检查一个值是否是 ref:
import { ref, isRef } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(isRef(count)); //
console.log(isRef(message)); // false
9. 最佳实践 | Best Practices
- 使用 script setup:简洁明了,推荐使用
- 组织逻辑:将相关的逻辑组合在一起
- 使用组合函数:复用逻辑,提高代码可维护性
- 合理使用响应式 API:根据需要选择 ref 或 reactive
- 避免过度使用 watch:优先使用 computed
- 注意响应式陷阱:了解响应式系统的工作原理,避免常见陷阱
10. 示例 | Examples
10.1 计数器示例
<template>
<div class="counter">
<h2>Counter</h2>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<div>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => (count.value = 0);
</script>
<style scoped>
.counter {
text-align: center;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 300px;
margin: 0 auto;
}
button {
margin: 0 5px;
padding: 5px 10px;
font-size: 16px;
}
</style>
10.2 表单示例
<template>
<div class="form">
<h2>Form</h2>
<div>
<label>Name:</label>
<input v-model="form.name" type="text" />
</div>
<div>
<label>Email:</label>
<input v-model="form.email" type="email" />
</div>
<div>
<label>Message:</label>
<textarea v-model="form.message"></textarea>
</div>
<button @click="submitForm">Submit</button>
<div v-if="submitted">
<h3>Submitted Data:</h3>
<pre>{{ form }}</pre>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
const form = reactive({
name: '',
email: '',
message: ''
}
const submitted = ref(false)
const submitForm = () => {
console.log('Form submitted:', form)
submitted.value =
}
</script>
<style scoped>
.form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
div {
margin-bottom: 10px;
}
label {
display: inline-block;
width: 80px;
}
input,
textarea {
width: 300px;
padding: 5px;
}
button {
margin-top: 10px;
padding: 5px 10px;
}
</style>
11. 小结 | Summary
组合式 API 是 Vue3 的重要特性,它提供了一种更灵活、更强大的方式来组织组件逻辑。通过本章节的学习,你已经了解了组合式 API 的基本概念和使用方法,包括 setup 函数、响应式 API、生命周期钩子、组合函数等。
组合式 API 的核心优势在于它允许你根据逻辑关注点组织代码,而不是根据选项类型。这使得代码更加模块化、可复用,并且更易于理解和维护。
在实际开发中,建议使用 script setup 语法糖,它使组合式 API 的使用更加简洁明了。同时,要善于使用组合函数来复用逻辑,提高代码的可维护性。
响应式状态
ref 响应式引用
const <state> = ref(<initialValue>);
import { ref } from 'vue';
const count = ref(0);
count.value++; // 修改值
console.log(count.value); // 读取值
const user = ref({ name: 'Tom' });
user.value.name = 'Jerry'; // 修改对象属性
reactive 对象响应式
const <state> = reactive(<object>);
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: { name: 'Tom' }
});
state.count++;
state.user.name = 'Jerry';
shallowRef 浅响应式引用
const <state> = shallowRef(<initialValue>);
import { shallowRef } from 'vue';
const obj = shallowRef({ count: 0 });
obj.value.count = 1; // 不会触发更新
obj.value = { count: 1 }; // 替换整个值才触发
shallowReactive 浅响应式对象
const <state> = shallowReactive(<object>);
import { shallowReactive } from 'vue';
const state = shallowReactive({ nested: { count: 0 } });
state.nested.count = 1; // 不会触发更新
readonly 只读代理
const <readonly> = readonly(<reactiveSource>);
import { reactive, readonly } from 'vue';
const original = reactive({ count: 0 });
const copy = readonly(original);
copy.count++; // 警告并失败
计算属性
computed 计算属性
const <result> = computed(() => <expression>);
import { ref, computed } from 'vue';
const count = ref(1);
const double = computed(() => count.value * 2);
console.log(double.value); // 2
可写 computed
const <result> = computed({ get, set });
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
get() { return `${firstName.value} ${lastName.value}`; },
set(val) {
[firstName.value, lastName.value] = val.split(' ');
}
});
fullName.value = 'Tom Smith';
侦听器
watch 侦听器
watch(<source>, (<newVal>, [oldVal]) => {}, [options]);
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变为 ${newVal}`);
});
watch(count, (newVal, oldVal, onCleanup) => {
const timer = setTimeout(() => doSomething(newVal), 500);
onCleanup(() => clearTimeout(timer));
});
watch 多源侦听
watch([fooRef, barRef], ([newFoo, newBar], [oldFoo, oldBar]) => {
console.log('foo 或 bar 变化');
});
watch(
() => state.user.name,
(newVal, oldVal) => console.log('name 变化')
);
watch 配置选项
watch(count, callback, {
immediate: true, // 立即执行
deep: true, // 深度侦听
flush: 'post', // 'pre' | 'post' | 'sync'
once: true // 只触发一次
});
watchEffect 自动追踪依赖
watchEffect(<effect> => {});
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log('count:', count.value);
});
watchEffect((onCleanup) => {
const timer = setInterval(() => console.log(count.value), 1000);
onCleanup(() => clearInterval(timer));
});
watchPostEffect DOM 更新后执行
import { watchPostEffect } from 'vue';
watchPostEffect(() => {
console.log('DOM 已更新');
});
watchSyncEffect 同步执行
import { watchSyncEffect } from 'vue';
watchSyncEffect(() => {
console.log('同步执行');
});
工具函数
toRef 转换为 ref
const <ref> = toRef(<source>, <key>);
import { reactive, toRef } from 'vue';
const state = reactive({ count: 0 });
const countRef = toRef(state, 'count');
countRef.value++; // 同步修改 state.count
toRefs 解构响应式对象
const { <key>, ... } = toRefs(<reactive>);
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Tom' });
const { count, name } = toRefs(state);
count.value++;
unref 获取值
const <value> = unref(<maybeRef>);
import { ref, unref } from 'vue';
const count = ref(0);
console.log(unref(count)); // 0
console.log(unref(123)); // 123
isRef / isReactive / isProxy
import { ref, reactive, isRef, isReactive, isProxy } from 'vue';
isRef(ref(0)); // true
isReactive(reactive({})); // true
isProxy(reactive({})); // true
toRaw 获取原始对象
const <raw> = toRaw(<proxy>);
import { reactive, toRaw } from 'vue';
const foo = reactive({});
const raw = toRaw(foo);
console.log(raw === foo); // false
markRaw 标记永不响应
const <obj> = markRaw(<object>);
import { reactive, markRaw } from 'vue';
const state = reactive({});
state.classInstance = markRaw(new SomeClass());
依赖注入
provide 提供
provide(<key>, <value>);
import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);
provide('theme', 'dark'); // 静态值
provide(Symbol('config'), {});
inject 注入
const <value> = inject(<key>, [defaultValue], [treatDefaultAsFactory]);
import { inject } from 'vue';
const theme = inject('theme');
const theme = inject('theme', 'light');
const config = inject('config', () => createDefaultConfig(), true);
模板引用
useTemplateRef 模板引用(Vue 3.5+)
const <el> = useTemplateRef(<refName>);
import { useTemplateRef } from 'vue';
const inputEl = useTemplateRef('inputRef');
onMounted(() => {
inputEl.value?.focus();
});
vue
<template>
<input ref="inputRef" />
</template>
ref 字符串方式(传统)
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => inputRef.value?.focus());
</script>
函数式 ref
<template>
<input :ref="(el) => { inputEl = el }" />
</template>
组件通信 API
defineProps 声明 props
const <props> = defineProps(<propsSpec>);
const props = defineProps({
title: String,
count: { type: Number, default: 0 },
list: { type: Array, required: true },
callback: { type: Function, default: () => {} }
});
defineProps 泛型方式
const props = defineProps<{
title: string;
count?: number;
list: string[];
}>();
响应式 props 解构(Vue 3.5+)
const { title, count = 0 } = defineProps<{
title: string;
count?: number;
}>();
// title 和 count 自动保持响应性
defineEmits 声明事件
const <emit> = defineEmits(<eventsSpec>);
const emit = defineEmits(['change', 'submit']);
emit('change', value);
emit('submit', { data: payload });
defineEmits 泛型方式
const emit = defineEmits<{
(e: 'change', value: string): void;
(e: 'submit', payload: { id: number }): void;
}>();
defineExpose 暴露方法
defineExpose({ <key>: <value>, ... });
const publicMethod = () => console.log('called');
defineExpose({ publicMethod, props });
defineModel 双向绑定(Vue 3.4+)
const <model> = defineModel([modelName], [options]);
const model = defineModel<string>();
function update() {
model.value = 'new value';
}
const title = defineModel<string>('title');
const count = defineModel<number>('count', { default: 0, local: true });
defineOptions 定义选项
defineOptions({
name: 'MyComponent',
inheritAttrs: false,
customOption: 'value'
});
defineSlots 类型声明
const slots = defineSlots<{
default(props: { item: any }): any;
header?(): any;
}>();
useAttrs 获取透传属性
const <attrs> = useAttrs();
import { useAttrs } from 'vue';
const attrs = useAttrs();
console.log(attrs.class, attrs.id);
useSlots 获取插槽
const <slots> = useSlots();
import { useSlots } from 'vue';
const slots = useSlots();
if (slots.header) {
// 处理插槽
}