01-Vue Composition API
📋 学习目标
掌握Composition API核心函数
理解响应式系统原理
学习组合式函数开发
掌握生命周期和副作用
🎯 setup函数
基本用法
<script>
import {ref, reactive} from 'vue';
export default {
setup() {
const count = ref(0);
const user = reactive({
name: 'John',
age: 30
});
function increment() {
count.value++;
}
return {
count,
user,
increment
};
}
};
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>User: {{ user.name }}, {{ user.age }}</p>
<button @click="increment">+1</button>
</div>
</template>
setup语法糖
<script setup>
import {ref, reactive} from 'vue';
const count = ref(0);
const user = reactive({
name: 'John',
age: 30
});
function increment() {
count.value++;
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
📦 响应式核心
ref
import {ref} from 'vue';
// 基本类型
const count = ref(0);
console.log(count.value); // 0
count.value++;
// 对象(会自动转为reactive)
const user = ref({name: 'John'});
user.value.name = 'Jane';
// 数组
const list = ref([1, 2, 3]);
list.value.push(4);
// 模板中自动解包
// <p>{{ count }}</p> 不需要.value
reactive
import {reactive} from 'vue';
// 响应式对象
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
}
});
state.count++; // 直接修改,不需要.value
// 响应式数组
const list = reactive([1, 2, 3]);
list.push(4);
// 响应式Map/Set
const map = reactive(new Map());
const set = reactive(new Set());
ref vs reactive
// ref:适合基本类型和单一值
const count = ref(0);
const name = ref('John');
// reactive:适合对象和复杂结构
const state = reactive({
count: 0,
name: 'John'
});
// 解构会失去响应性
const {count, name} = reactive({count: 0, name: 'John'}); // ❌
count++; // 不会触发更新
// 使用toRefs保持响应性
import {toRefs} from 'vue';
const state = reactive({count: 0, name: 'John'});
const {count, name} = toRefs(state); // ✅
count.value++; // 会触发更新
🔄 computed
基本用法
<script setup>
import {ref, computed} from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读computed
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 可写computed
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const parts = value.split(' ');
firstName.value = parts[0];
lastName.value = parts[1];
}
});
</script>
<template>
<div>
<input v-model="firstName" />
<input v-model="lastName" />
<p>{{ fullName }}</p>
<input v-model="fullName" />
</div>
</template>
👁️ watch与watchEffect
watch
import {ref, watch} from 'vue';
const count = ref(0);
// 监听单个ref
watch(count, (newVal, oldVal) => {
console.log(`Count: ${oldVal} -> ${newVal}`);
});
// 监听多个源
const name = ref('John');
const age = ref(30);
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
console.log(`Name: ${oldName} -> ${newName}`);
console.log(`Age: ${oldAge} -> ${newAge}`);
});
// 监听reactive对象
const state = reactive({count: 0});
watch(() => state.count, (newVal, oldVal) => {
console.log(`Count: ${oldVal} -> ${newVal}`);
});
// 深度监听
watch(state, (newVal) => {
console.log('State changed:', newVal);
}, {deep: true});
// 立即执行
watch(count, (val) => {
console.log('Count:', val);
}, {immediate: true});
watchEffect
import {ref, watchEffect} from 'vue';
const count = ref(0);
const double = ref(0);
// 自动追踪依赖
watchEffect(() => {
double.value = count.value * 2;
console.log(`Count: ${count.value}, Double: ${double.value}`);
});
// 清理副作用
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
onCleanup(() => {
clearInterval(timer);
});
});
// 停止监听
const stop = watchEffect(() => {
console.log(count.value);
});
// 手动停止
stop();
🔁 生命周期
Composition API生命周期
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
onBeforeMount(() => {
console.log('组件挂载前');
});
onMounted(() => {
console.log('组件挂载后');
// DOM已渲染,可以访问DOM
});
onBeforeUpdate(() => {
console.log('组件更新前');
});
onUpdated(() => {
console.log('组件更新后');
});
onBeforeUnmount(() => {
console.log('组件卸载前');
});
onUnmounted(() => {
console.log('组件卸载后');
// 清理定时器、事件监听等
});
</script>
🎁 Provide/Inject
基本用法
<!-- 父组件 -->
<script setup>
import {provide, ref} from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme) => {
theme.value = newTheme;
};
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>
<!-- 子组件 -->
<script setup>
import {inject} from 'vue';
const theme = inject('theme');
const updateTheme = inject('updateTheme');
</script>
<template>
<div :class="theme">
<button @click="updateTheme('light')">Light</button>
<button @click="updateTheme('dark')">Dark</button>
</div>
</template>
类型化Provide/Inject
import {InjectionKey, provide, inject} from 'vue';
interface Theme {
primary: string;
secondary: string;
}
const themeKey: InjectionKey<Theme> = Symbol('theme');
// 提供
provide(themeKey, {
primary: '#007bff',
secondary: '#6c757d'
});
// 注入
const theme = inject(themeKey);
// theme的类型为Theme | undefined
🛠️ 组合式函数
基本组合式函数
// useCounter.js
import {ref} from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function reset() {
count.value = initialValue;
}
return {
count,
increment,
decrement,
reset
};
}
<!-- 使用 -->
<script setup>
import {useCounter} from './useCounter';
const {count, increment, decrement, reset} = useCounter(10);
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
实用组合式函数
// useFetch.js
import {ref} from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
async function fetchData() {
loading.value = true;
try {
const response = await fetch(url);
data.value = await response.json();
error.value = null;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
fetchData();
return {data, error, loading, refetch: fetchData};
}
// useLocalStorage.js
import {ref, watch} from 'vue';
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue);
// 初始化
const stored = localStorage.getItem(key);
if (stored) {
value.value = JSON.parse(stored);
}
// 监听变化
watch(value, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal));
}, {deep: true});
return value;
}
// useDebounce.js
import {ref, watch} from 'vue';
export function useDebounce(value, delay = 500) {
const debouncedValue = ref(value.value);
watch(value, (newVal) => {
const timer = setTimeout(() => {
debouncedValue.value = newVal;
}, delay);
return () => clearTimeout(timer);
});
return debouncedValue;
}
📱 Props和Emits
defineProps
<script setup>
// 基本用法
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
},
user: {
type: Object,
required: true
}
});
// TypeScript类型
interface Props {
title: string;
count?: number;
user: {
name: string;
age: number;
};
}
const props = defineProps<Props>();
// 默认值(TypeScript)
const props = withDefaults(defineProps<Props>(), {
count: 0
});
</script>
defineEmits
<script setup>
// 基本用法
const emit = defineEmits(['update', 'delete']);
function handleClick() {
emit('update', {id: 1});
}
// TypeScript类型
const emit = defineEmits<{
update: [id: number];
delete: [id: number, confirmed: boolean];
}>();
emit('update', 123);
emit('delete', 456, true);
</script>
<template>
<button @click="handleClick">Update</button>
</template>
🎯 defineExpose
暴露组件方法
<script setup>
import {ref} from 'vue';
const count = ref(0);
const internalValue = ref('secret');
function increment() {
count.value++;
}
// 只暴露特定属性和方法
defineExpose({
count,
increment
});
// internalValue不会暴露
</script>
<!-- 父组件 -->
<script setup>
import {ref} from 'vue';
import ChildComponent from './ChildComponent.vue';
const childRef = ref();
function callChild() {
childRef.value.increment();
console.log(childRef.value.count);
}
</script>
<template>
<ChildComponent ref="childRef" />
<button @click="callChild">Call Child</button>
</template>
📚 实践练习
练习1:组合式函数
实现以下组合式函数:
useToggle:布尔值切换
useArray:数组操作
useMouse:鼠标位置追踪
练习2:表单管理
使用Composition API实现:
表单验证
错误提示
提交处理
练习3:数据获取
创建可复用的数据获取Hook:
加载状态
错误处理
重新请求
缓存
📚 参考资料
VueUse - 组合式函数集合