Composition API 是 Vue 3 引入的一套新的 API 风格,它允许开发者使用函数式的方式组织和复用组件逻辑。与 Options API 不同,Composition API 基于逻辑关注点而不是选项类型来组织代码。
从 Vue 导入需要的函数
在 setup() 中组织逻辑
返回模板需要的数据和方法
// Options API 示例
export default {
data() {
return {
count: 0,
message: 'Hello'
};
},
computed: {
doubledCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
},
updateMessage(newMsg) {
this.message = newMsg;
}
},
mounted() {
console.log('组件已挂载');
}
};
// Composition API 示例
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
// 响应式数据
const count = ref(0);
const message = ref('Hello');
// 计算属性
const doubledCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
const updateMessage = (newMsg) => {
message.value = newMsg;
};
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载');
});
// 返回模板需要的内容
return {
count,
message,
doubledCount,
increment,
updateMessage
};
}
};
相关逻辑可以组织在一起,而不是分散在不同的选项中
逻辑可以提取为可重用的组合式函数
对 TypeScript 有更好的支持
Tree-shaking 友好,只导入需要的函数
在 Options API 中,相关逻辑被分散到不同的选项中:
data() 中methods 中computed 中watch 中当组件变得复杂时,理解和维护变得困难。
Options API 主要通过以下方式复用代码:
这些方式都有各自的局限性和缺点。
// 用户相关逻辑 - 分散在各处
export default {
data() {
return {
// 用户数据
user: null,
isLoadingUser: false,
// 文章数据
posts: [],
isLoadingPosts: false
};
},
computed: {
// 用户计算属性
userName() {
return this.user?.name || '游客';
},
// 文章计算属性
postCount() {
return this.posts.length;
}
},
methods: {
// 用户方法
async fetchUser() {
this.isLoadingUser = true;
this.user = await api.getUser();
this.isLoadingUser = false;
},
// 文章方法
async fetchPosts() {
this.isLoadingPosts = true;
this.posts = await api.getPosts();
this.isLoadingPosts = false;
}
},
watch: {
// 用户侦听器
'user.id'(newId) {
if (newId) {
this.fetchPosts();
}
}
},
mounted() {
// 用户生命周期
this.fetchUser();
}
};
// 用户相关逻辑 - 集中在一起
import { ref, computed, watch, onMounted } from 'vue';
export default {
setup() {
// ========== 用户逻辑 ==========
const user = ref(null);
const isLoadingUser = ref(false);
const userName = computed(() => user.value?.name || '游客');
const fetchUser = async () => {
isLoadingUser.value = true;
user.value = await api.getUser();
isLoadingUser.value = false;
};
// 用户相关生命周期
onMounted(fetchUser);
// ========== 文章逻辑 ==========
const posts = ref([]);
const isLoadingPosts = ref(false);
const postCount = computed(() => posts.value.length);
const fetchPosts = async () => {
isLoadingPosts.value = true;
posts.value = await api.getPosts();
isLoadingPosts.value = false;
};
// 文章相关侦听器
watch(
() => user.value?.id,
(newId) => {
if (newId) {
fetchPosts();
}
}
);
return {
// 用户相关
user,
isLoadingUser,
userName,
fetchUser,
// 文章相关
posts,
isLoadingPosts,
postCount,
fetchPosts
};
}
};
关键优势:
setup() 函数是 Composition API 的入口点。它在组件创建之前执行,用于定义响应式数据、计算属性、方法等。
import { ref, computed } from 'vue';
export default {
// setup 函数在组件创建之前执行
setup() {
// 定义响应式数据
const count = ref(0);
const message = ref('Hello Vue 3');
// 定义计算属性
const reversedMessage = computed(() => {
return message.value.split('').reverse().join('');
});
// 定义方法
const increment = () => {
count.value++;
};
const updateMessage = (newMsg) => {
message.value = newMsg;
};
// 返回的数据和方法可以在模板中使用
return {
count,
message,
reversedMessage,
increment,
updateMessage
};
},
// 模板中使用
template: `
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
<p>消息: {{ message }}</p>
<p>反转消息: {{ reversedMessage }}</p>
<input :value="message" @input="updateMessage($event.target.value)">
</div>
`
};
import { ref, watch } from 'vue';
export default {
props: {
// 定义props
initialCount: {
type: Number,
default: 0
},
title: {
type: String,
required: true
}
},
emits: ['count-change'], // 定义自定义事件
// setup 函数接收两个参数
setup(props, context) {
// 第一个参数: props - 响应式的props对象
console.log('props:', props);
console.log('initialCount:', props.initialCount);
console.log('title:', props.title);
// props是响应式的,可以使用watch监听
watch(
() => props.initialCount,
(newVal, oldVal) => {
console.log(`initialCount从${oldVal}变为${newVal}`);
}
);
// 定义响应式数据,基于props
const count = ref(props.initialCount);
// 第二个参数: context - 上下文对象
// context包含以下属性:
// - attrs: 非响应式的属性对象
// - slots: 插槽对象
// - emit: 触发事件的函数
// - expose: 暴露公共属性的函数
const increment = () => {
count.value++;
// 使用context.emit触发自定义事件
context.emit('count-change', count.value);
};
// 返回模板需要的内容
return {
count,
increment,
title: props.title // 可以直接返回props中的值
};
}
};
import { ref } from 'vue';
export default {
setup(props, context) {
// context.attrs - 包含所有非props的属性
console.log('attrs:', context.attrs);
// 例如: <MyComponent id="my-id" class="my-class" data-test="test" />
// id、class、data-test都会在attrs中
// context.slots - 包含所有插槽
console.log('slots:', context.slots);
// 可以访问具名插槽和默认插槽
// context.slots.default() - 默认插槽内容
// context.slots.header() - 名为header的插槽内容
// context.emit - 触发自定义事件
const handleClick = () => {
context.emit('custom-event', { data: 'some data' });
};
// context.expose - 暴露公共属性给父组件
const internalData = ref('内部数据');
// 暴露给父组件的内容
context.expose({
getInternalData: () => internalData.value,
resetData: () => {
internalData.value = '重置后的数据';
}
});
// 只有通过expose暴露的内容才能被父组件访问
// 父组件可以通过ref访问: this.$refs.myComponent.getInternalData()
return {
handleClick
};
}
};
<!-- 使用 <script setup> 语法糖 -->
<script setup>
// 这个脚本会在组件每次创建时执行
// 所有内容都会自动暴露给模板
import { ref, computed, onMounted } from 'vue';
// 定义props
const props = defineProps({
initialCount: {
type: Number,
default: 0
},
title: String
});
// 定义emit
const emit = defineEmits(['count-change']);
// 响应式数据
const count = ref(props.initialCount);
const message = ref('Hello Vue 3');
// 计算属性
const doubledCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
emit('count-change', count.value);
};
const updateMessage = (newMsg) => {
message.value = newMsg;
};
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载');
});
// 不需要return,所有内容自动可用
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>计数: {{ count }}</p>
<p>双倍计数: {{ doubledCount }}</p>
<button @click="increment">增加</button>
<p>消息: {{ message }}</p>
<input :value="message" @input="updateMessage($event.target.value)">
</div>
</template>
<style scoped>
/* 样式 */
</style>
setup() 函数中,无法访问 this。组件实例在 setup() 执行时尚未创建。所有组件选项(如 data、methods 等)都需要在 setup() 中定义。
Composition API 提供了两种创建响应式数据的方式:ref() 和 reactive()。
ref() - 创建响应式引用
用途: 创建单个值的响应式引用
特点:
.value 访问和修改值.value
import { ref } from 'vue';
const count = ref(0); // 原始类型
const user = ref({ name: 'John' }); // 对象
// 修改值
count.value = 1;
user.value.name = 'Jane';
// 在模板中使用
// <p>{{ count }}</p> 自动解包,不需要 .value
reactive() - 创建响应式对象
用途: 创建响应式的对象或数组
特点:
.value
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: { name: 'John' },
items: ['item1', 'item2']
});
// 直接修改属性
state.count = 1;
state.user.name = 'Jane';
state.items.push('item3');
// 在模板中使用
// <p>{{ state.count }}</p>
| 特性 | ref() |
reactive() |
|---|---|---|
| 包装类型 | 任何类型 | 仅对象类型 |
| 访问方式 | 通过 .value 属性 |
直接访问属性 |
| 模板使用 | 自动解包,不需要 .value |
直接访问属性 |
| 解构/展开 | 使用 toRefs() 保持响应性 |
解构会失去响应性 |
| TypeScript 支持 | 类型推断良好 | 类型推断良好 |
| 适用场景 | 原始值、单个值、需要明确引用的场景 | 复杂对象、表单数据、状态对象 |
toRef()
为响应式对象的属性创建 ref
import { reactive, toRef } from 'vue';
const state = reactive({ count: 0 });
const countRef = toRef(state, 'count');
// countRef 是 state.count 的引用
countRef.value++; // state.count 也会更新
toRefs()
将响应式对象转换为普通对象,每个属性都是 ref
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
name: 'Vue'
});
// 解构时保持响应性
const { count, name } = toRefs(state);
// 在模板中可以直接使用 count, name
readonly()
创建只读的响应式对象
import { reactive, readonly } from 'vue';
const original = reactive({ count: 0 });
const copy = readonly(original);
// copy.count++ // 错误: 只读属性
original.count++; // 可以修改原始对象
<script setup>
import { ref, reactive, toRefs, computed } from 'vue';
// 使用 ref 创建基本数据
const isLoading = ref(false);
const error = ref(null);
// 使用 reactive 创建复杂状态
const state = reactive({
user: {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
profile: {
age: 25,
city: '北京'
}
},
posts: [
{ id: 1, title: '第一篇帖子', content: '内容...' },
{ id: 2, title: '第二篇帖子', content: '内容...' }
],
settings: {
theme: 'light',
notifications: true
}
});
// 使用 toRefs 解构,保持响应性
const { user, posts, settings } = toRefs(state);
// 计算属性
const postCount = computed(() => posts.value.length);
const fullUserInfo = computed(() => {
return `${user.value.name} (${user.value.profile.age}岁, ${user.value.profile.city})`;
});
// 方法
const updateUserName = (newName) => {
user.value.name = newName;
};
const addPost = () => {
const newId = posts.value.length + 1;
posts.value.push({
id: newId,
title: `新帖子 ${newId}`,
content: '这是新帖子的内容...'
});
};
const toggleTheme = () => {
settings.value.theme = settings.value.theme === 'light' ? 'dark' : 'light';
};
// 异步操作示例
const fetchData = async () => {
isLoading.value = true;
error.value = null;
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 更新数据
user.value.name = '更新后的名字';
posts.value.push({
id: posts.value.length + 1,
title: '异步加载的帖子',
content: '这是异步加载的内容...'
});
} catch (err) {
error.value = err.message;
} finally {
isLoading.value = false;
}
};
</script>
<template>
<div>
<h2>用户信息</h2>
<p>{{ fullUserInfo }}</p>
<button @click="updateUserName('李四')">更新用户名</button>
<h3>帖子列表 ({{ postCount }})</h3>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
<button @click="addPost">添加帖子</button>
<h3>设置</h3>
<p>主题: {{ settings.theme }}</p>
<button @click="toggleTheme">切换主题</button>
<div v-if="isLoading">加载中...</div>
<div v-if="error" class="error">错误: {{ error }}</div>
<button @click="fetchData" :disabled="isLoading">
{{ isLoading ? '加载中...' : '获取数据' }}
</button>
</div>
</template>
ref() 就足够了。当你有多个相关属性需要组织在一起时,可以使用 reactive()。记住,解构 reactive() 对象会失去响应性,需要使用 toRefs()。
import { ref, computed } from 'vue';
const count = ref(0);
const price = ref(10);
// 只读计算属性
const total = computed(() => {
return count.value * price.value;
});
// 在模板中使用
// <p>总价: {{ total }}</p>
import { ref, computed } from 'vue';
const firstName = ref('张');
const lastName = ref('三');
// 可写计算属性
const fullName = computed({
// getter
get() {
return `${firstName.value} ${lastName.value}`;
},
// setter
set(newValue) {
const names = newValue.split(' ');
firstName.value = names[0];
lastName.value = names[1] || '';
}
});
// 使用setter
fullName.value = '李 四';
watch() - 精确侦听
特点:
import { ref, watch } from 'vue';
const count = ref(0);
const user = ref({ name: 'John', age: 25 });
// 侦听单个ref
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`);
});
// 侦听getter函数
watch(
() => user.value.name,
(newName, oldName) => {
console.log(`用户名从${oldName}变为${newName}`);
}
);
// 侦听多个源
watch([count, () => user.value.age],
([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${oldCount}→${newCount}`);
console.log(`age: ${oldAge}→${newAge}`);
}
);
// 深度侦听对象
watch(
user,
(newUser, oldUser) => {
console.log('user对象变化:', newUser);
},
{ deep: true } // 深度侦听
);
// 立即执行
watch(count, (newVal, oldVal) => {
console.log('立即执行:', newVal);
}, { immediate: true });
watchEffect() - 自动侦听
特点:
import { ref, watchEffect } from 'vue';
const count = ref(0);
const price = ref(10);
// 自动侦听函数中使用的响应式数据
watchEffect(() => {
console.log(`watchEffect执行,count=${count.value}`);
const total = count.value * price.value;
console.log(`总价: ${total}`);
// 副作用:更新DOM或调用API
document.title = `计数: ${count.value}`;
});
// 当count或price变化时,watchEffect会自动重新执行
// 停止侦听
const stop = watchEffect(() => {
// 侦听逻辑
});
// 手动停止
stop();
// 清理副作用
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('定时器执行');
}, 1000);
// 清理函数
onCleanup(() => {
clearTimeout(timer);
console.log('清理定时器');
});
});
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue';
// 表单数据
const form = reactive({
firstName: '张',
lastName: '三',
age: 25,
email: 'zhangsan@example.com'
});
// 计算属性
const fullName = computed(() => {
return `${form.firstName} ${form.lastName}`;
});
const isAdult = computed(() => {
return form.age >= 18;
});
const emailDomain = computed(() => {
return form.email.split('@')[1] || '';
});
// 使用watch精确侦听
watch(
() => form.age,
(newAge, oldAge) => {
console.log(`年龄从${oldAge}变为${newAge}`);
if (newAge >= 18 && oldAge < 18) {
console.log('恭喜!您现在是成年人了。');
}
}
);
// 侦听多个字段
watch(
[() => form.firstName, () => form.lastName],
([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
console.log(`姓名从${oldFirstName} ${oldLastName}变为${newFirstName} ${newLastName}`);
}
);
// 使用watchEffect自动侦听
const stopWatch = watchEffect(() => {
console.log(`表单数据更新: ${fullName.value}, ${form.age}岁`);
// 验证逻辑
if (form.age < 0) {
console.error('年龄不能为负数');
}
if (!form.email.includes('@')) {
console.error('邮箱格式不正确');
}
});
// 方法
const updateAge = () => {
form.age += 1;
};
const resetForm = () => {
form.firstName = '张';
form.lastName = '三';
form.age = 25;
form.email = 'zhangsan@example.com';
};
// 停止watchEffect
const stopEffect = () => {
stopWatch();
console.log('已停止自动侦听');
};
</script>
<template>
<div>
<h2>用户信息</h2>
<p>全名: {{ fullName }}</p>
<p>年龄: {{ form.age }} <span v-if="isAdult">(成年人)</span></p>
<p>邮箱: {{ form.email }} <span v-if="emailDomain">(域名: {{ emailDomain }})</span></p>
<div class="form-group">
<label>名:</label>
<input v-model="form.firstName">
</div>
<div class="form-group">
<label>姓:</label>
<input v-model="form.lastName">
</div>
<div class="form-group">
<label>年龄:</label>
<input v-model.number="form.age" type="number">
</div>
<div class="form-group">
<label>邮箱:</label>
<input v-model="form.email" type="email">
</div>
<button @click="updateAge">增加年龄</button>
<button @click="resetForm">重置表单</button>
<button @click="stopEffect">停止自动侦听</button>
</div>
</template>
watch() 当你需要精确控制侦听什么数据以及何时执行;使用 watchEffect() 当你需要自动侦听函数中使用的所有响应式数据,并且需要立即执行副作用。
在 Composition API 中,生命周期钩子是通过函数形式导入和使用的。
| Options API | Composition API | 执行时机 |
|---|---|---|
beforeCreate |
不需要(在 setup 中替代) | 实例初始化之后,数据观测之前 |
created |
不需要(在 setup 中替代) | 实例创建完成后 |
beforeMount |
onBeforeMount |
挂载开始之前 |
mounted |
onMounted |
挂载完成后 |
beforeUpdate |
onBeforeUpdate |
数据更新时,DOM 打补丁之前 |
updated |
onUpdated |
数据更新后,DOM 打补丁之后 |
beforeUnmount |
onBeforeUnmount |
卸载之前 |
unmounted |
onUnmounted |
卸载完成后 |
errorCaptured |
onErrorCaptured |
捕获后代组件错误时 |
activated |
onActivated |
被 keep-alive 缓存的组件激活时 |
deactivated |
onDeactivated |
被 keep-alive 缓存的组件停用时 |
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
ref
} from 'vue';
export default {
setup() {
const count = ref(0);
// 挂载阶段
onBeforeMount(() => {
console.log('组件即将挂载');
});
onMounted(() => {
console.log('组件已挂载');
// 可以访问DOM元素
console.log('DOM元素:', document.getElementById('app'));
});
// 更新阶段
onBeforeUpdate(() => {
console.log('组件即将更新');
console.log('当前count:', count.value);
});
onUpdated(() => {
console.log('组件已更新');
console.log('更新后count:', count.value);
});
// 卸载阶段
onBeforeUnmount(() => {
console.log('组件即将卸载');
// 清理工作
});
onUnmounted(() => {
console.log('组件已卸载');
});
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err);
console.error('错误信息:', info);
// 返回false阻止错误继续向上传播
return false;
});
const increment = () => {
count.value++;
};
return { count, increment };
}
};
<script setup>
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue';
// 数据
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);
// 定时器
const timer = ref(null);
const seconds = ref(0);
// 事件监听
const mousePosition = ref({ x: 0, y: 0 });
// 组件挂载时
onMounted(async () => {
console.log('组件挂载,开始获取数据');
// 获取初始数据
await fetchData();
// 启动定时器
startTimer();
// 添加事件监听
window.addEventListener('mousemove', updateMousePosition);
});
// 组件卸载时
onUnmounted(() => {
console.log('组件卸载,清理资源');
// 清除定时器
if (timer.value) {
clearInterval(timer.value);
}
// 移除事件监听
window.removeEventListener('mousemove', updateMousePosition);
});
// keep-alive 相关
onActivated(() => {
console.log('组件激活');
// 重新启动定时器
startTimer();
});
onDeactivated(() => {
console.log('组件停用');
// 暂停定时器
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
});
// 方法
const fetchData = async () => {
isLoading.value = true;
error.value = null;
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
data.value = { message: '数据加载成功', timestamp: new Date() };
} catch (err) {
error.value = '数据加载失败';
} finally {
isLoading.value = false;
}
};
const startTimer = () => {
if (timer.value) {
clearInterval(timer.value);
}
timer.value = setInterval(() => {
seconds.value++;
}, 1000);
};
const updateMousePosition = (event) => {
mousePosition.value = {
x: event.clientX,
y: event.clientY
};
};
</script>
<template>
<div>
<h3>生命周期示例</h3>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="data">
<p>{{ data.message }}</p>
<p>时间: {{ data.timestamp }}</p>
</div>
<p>定时器: {{ seconds }} 秒</p>
<p>鼠标位置: X={{ mousePosition.x }}, Y={{ mousePosition.y }}</p>
</div>
</template>
setup() 函数中同步调用。不要在异步函数中调用它们,否则可能无法正确注册。如果需要异步操作,可以在钩子内部使用异步函数。
在 Composition API 中,依赖注入通过 provide() 和 inject() 函数实现。
provide() - 提供依赖
import { provide, ref, reactive, computed } from 'vue';
export default {
setup() {
// 提供基本值
provide('appName', '我的应用');
// 提供响应式数据
const user = ref({ name: '张三', age: 25 });
provide('user', user);
// 提供响应式对象
const theme = reactive({
mode: 'dark',
colors: {
primary: '#42b983',
secondary: '#3498db'
}
});
provide('theme', theme);
// 提供计算属性
const isDarkMode = computed(() => theme.mode === 'dark');
provide('isDarkMode', isDarkMode);
// 提供方法
const updateTheme = (newMode) => {
theme.mode = newMode;
};
provide('updateTheme', updateTheme);
return {};
}
};
inject() - 注入依赖
import { inject, ref, computed } from 'vue';
export default {
setup() {
// 注入基本值
const appName = inject('appName');
console.log('应用名称:', appName);
// 注入响应式数据
const user = inject('user');
console.log('用户:', user.value);
// 注入响应式对象
const theme = inject('theme');
console.log('主题:', theme);
// 注入计算属性
const isDarkMode = inject('isDarkMode');
console.log('是否为暗黑模式:', isDarkMode.value);
// 注入方法
const updateTheme = inject('updateTheme');
// 提供默认值
const config = inject('config', { theme: 'light', lang: 'zh' });
// 如果依赖必须存在
const requiredDep = inject('requiredDep');
if (!requiredDep) {
throw new Error('requiredDep is required');
}
// 本地方法
const toggleTheme = () => {
if (updateTheme) {
updateTheme(theme.mode === 'dark' ? 'light' : 'dark');
}
};
return {
appName,
user,
theme,
isDarkMode,
config,
toggleTheme
};
}
};
<!-- 根组件: App.vue -->
<script setup>
import { provide, ref, reactive, computed } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 应用主题
const theme = reactive({
mode: 'light',
colors: {
primary: '#42b983',
secondary: '#3498db',
background: '#ffffff',
text: '#333333'
}
});
// 用户信息
const currentUser = ref({
id: 1,
name: '张三',
role: 'admin',
preferences: {
notifications: true,
language: 'zh-CN'
}
});
// 应用配置
const appConfig = reactive({
version: '1.0.0',
apiUrl: 'https://api.example.com',
features: {
darkMode: true,
multiLanguage: true,
analytics: false
}
});
// 计算属性
const isAuthenticated = computed(() => !!currentUser.value.id);
const isAdmin = computed(() => currentUser.value.role === 'admin');
// 方法
const toggleTheme = () => {
theme.mode = theme.mode === 'light' ? 'dark' : 'light';
// 更新颜色
if (theme.mode === 'dark') {
theme.colors.background = '#1a1a1a';
theme.colors.text = '#ffffff';
} else {
theme.colors.background = '#ffffff';
theme.colors.text = '#333333';
}
};
const updateUser = (newUserData) => {
Object.assign(currentUser.value, newUserData);
};
// 提供依赖
provide('theme', theme);
provide('currentUser', currentUser);
provide('appConfig', appConfig);
provide('isAuthenticated', isAuthenticated);
provide('isAdmin', isAdmin);
provide('toggleTheme', toggleTheme);
provide('updateUser', updateUser);
</script>
<template>
<div :style="{
backgroundColor: theme.colors.background,
color: theme.colors.text,
padding: '20px',
minHeight: '100vh'
}">
<h1>我的应用</h1>
<button @click="toggleTheme">
切换主题 (当前: {{ theme.mode }})
</button>
<ChildComponent />
</div>
</template>
<!-- 子组件: ChildComponent.vue -->
<script setup>
import { inject, computed } from 'vue';
import GrandChildComponent from './GrandChildComponent.vue';
// 注入依赖
const theme = inject('theme');
const currentUser = inject('currentUser');
const appConfig = inject('appConfig');
const isAuthenticated = inject('isAuthenticated');
const isAdmin = inject('isAdmin');
const toggleTheme = inject('toggleTheme');
const updateUser = inject('updateUser');
// 本地计算属性
const userInfo = computed(() => {
return `${currentUser.value.name} (${currentUser.value.role})`;
});
const themeInfo = computed(() => {
return `主题: ${theme.mode}, 主色: ${theme.colors.primary}`;
});
// 本地方法
const updateUserName = () => {
const newName = prompt('请输入新用户名:', currentUser.value.name);
if (newName) {
updateUser({ name: newName });
}
};
</script>
<template>
<div style="margin: 20px 0; padding: 15px; border: 1px solid #ccc;">
<h2>子组件</h2>
<div>
<p>{{ userInfo }}</p>
<p>认证状态: {{ isAuthenticated ? '已认证' : '未认证' }}</p>
<p>管理员: {{ isAdmin ? '是' : '否' }}</p>
<button @click="updateUserName">更新用户名</button>
</div>
<div>
<p>{{ themeInfo }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
<div>
<p>应用版本: {{ appConfig.version }}</p>
<p>API地址: {{ appConfig.apiUrl }}</p>
</div>
<!-- 孙子组件 -->
<GrandChildComponent />
</div>
</template>
<!-- 孙子组件: GrandChildComponent.vue -->
<script setup>
import { inject } from 'vue';
// 注入依赖(可以跳过中间组件)
const theme = inject('theme');
const currentUser = inject('currentUser');
const toggleTheme = inject('toggleTheme');
const isAdmin = inject('isAdmin', false); // 提供默认值
</script>
<template>
<div style="margin: 15px 0; padding: 10px; background-color: rgba(0,0,0,0.05);">
<h3>孙子组件</h3>
<p>当前用户: {{ currentUser.name }}</p>
<p>主题模式: {{ theme.mode }}</p>
<p>是否是管理员: {{ isAdmin ? '是' : '否' }}</p>
<button @click="toggleTheme">从孙子组件切换主题</button>
</div>
</template>
在 Composition API 中,模板引用通过 ref() 函数创建,这与响应式 ref() 是同一个函数。
<script setup>
import { ref, onMounted } from 'vue';
// 创建模板引用
const inputRef = ref(null); // 初始值为null
const divRef = ref(null);
const buttonRef = ref(null);
// 组件挂载后,引用会被自动填充
onMounted(() => {
console.log('input元素:', inputRef.value);
console.log('div元素:', divRef.value);
console.log('button元素:', buttonRef.value);
// 可以立即操作元素
if (inputRef.value) {
inputRef.value.focus();
}
});
// 方法
const focusInput = () => {
if (inputRef.value) {
inputRef.value.focus();
inputRef.value.select();
}
};
const changeDivColor = () => {
if (divRef.value) {
divRef.value.style.backgroundColor = '#ffeb3b';
divRef.value.style.padding = '20px';
}
};
const simulateClick = () => {
if (buttonRef.value) {
buttonRef.value.click();
}
};
</script>
<template>
<div>
<!-- 绑定引用 -->
<input ref="inputRef" type="text" placeholder="请输入内容">
<button @click="focusInput">聚焦输入框</button>
<div ref="divRef" style="padding: 10px; border: 1px solid #ccc; margin: 10px 0;">
这是一个可修改样式的div
</div>
<button @click="changeDivColor">修改div样式</button>
<button ref="buttonRef" @click="alert('按钮被点击了!')">
原始按钮
</button>
<button @click="simulateClick">模拟点击原始按钮</button>
</div>
</template>
<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 引用子组件
const childRef = ref(null);
onMounted(() => {
console.log('子组件实例:', childRef.value);
// 访问子组件的公共方法
if (childRef.value) {
console.log('子组件数据:', childRef.value.getData());
// 调用子组件方法
childRef.value.sayHello();
}
});
// 调用子组件方法
const callChildMethod = () => {
if (childRef.value) {
childRef.value.increment();
}
};
const getChildData = () => {
if (childRef.value) {
const data = childRef.value.getData();
alert(`子组件数据: count=${data.count}, message=${data.message}`);
}
};
</script>
<template>
<div>
<h3>父组件</h3>
<button @click="callChildMethod">调用子组件方法</button>
<button @click="getChildData">获取子组件数据</button>
<!-- 引用子组件 -->
<ChildComponent ref="childRef" />
</div>
</template>
<!-- 子组件: ChildComponent.vue -->
<script setup>
import { ref, defineExpose } from 'vue';
const count = ref(0);
const message = ref('Hello from child');
const increment = () => {
count.value++;
};
const sayHello = () => {
alert('Hello!');
};
const getData = () => {
return {
count: count.value,
message: message.value
};
};
// 暴露给父组件的内容
defineExpose({
increment,
sayHello,
getData,
count, // 也可以暴露响应式数据(不推荐)
message
});
</script>
<template>
<div style="padding: 20px; border: 2px solid #42b983; margin: 10px 0;">
<h4>子组件</h4>
<p>计数: {{ count }}</p>
<p>消息: {{ message }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
// 模板引用
const elementRef = ref(null);
const inputRef = ref(null);
// 响应式数据
const elementInfo = ref('元素未加载');
const inputValue = ref('');
// 监听引用变化
watch(elementRef, (newElement, oldElement) => {
console.log('元素引用变化:', { old: oldElement, new: newElement });
if (newElement) {
elementInfo.value = `元素已加载,标签名: ${newElement.tagName}`;
// 可以立即操作元素
newElement.style.border = '2px solid #3498db';
newElement.style.padding = '10px';
} else if (oldElement) {
elementInfo.value = '元素已卸载';
}
}, {
immediate: true // 立即执行一次
});
// 监听输入框引用变化
watch(inputRef, (newInput) => {
if (newInput) {
console.log('输入框已加载');
newInput.focus();
// 监听输入框输入事件
newInput.addEventListener('input', (event) => {
inputValue.value = event.target.value;
});
}
});
// 动态修改样式
const changeElementStyle = () => {
if (elementRef.value) {
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
elementRef.value.style.backgroundColor = randomColor;
elementRef.value.style.color = 'white';
elementRef.value.style.fontWeight = 'bold';
}
};
// 清除输入框
const clearInput = () => {
if (inputRef.value) {
inputRef.value.value = '';
inputRef.value.focus();
inputValue.value = '';
}
};
</script>
<template>
<div>
<h3>响应式模板引用</h3>
<div ref="elementRef" style="margin: 10px 0; padding: 15px;">
<p>{{ elementInfo }}</p>
</div>
<button @click="changeElementStyle">修改元素样式</button>
<div style="margin: 20px 0;">
<input ref="inputRef" type="text" placeholder="输入内容...">
<button @click="clearInput" style="margin-left: 10px;">清除</button>
</div>
<p>输入的值: {{ inputValue }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 项目列表
const items = ref([
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' },
{ id: 4, name: '项目4' }
]);
// 引用数组
const itemRefs = ref([]);
// 动态添加项目
const addItem = () => {
const newId = items.value.length + 1;
items.value.push({ id: newId, name: `项目${newId}` });
};
// 移除项目
const removeItem = (index) => {
items.value.splice(index, 1);
};
// 高亮指定项目
const highlightItem = (index) => {
if (itemRefs.value[index]) {
// 先清除所有高亮
clearHighlights();
// 高亮指定项目
const element = itemRefs.value[index];
element.style.backgroundColor = '#ffeb3b';
element.style.fontWeight = 'bold';
}
};
// 清除所有高亮
const clearHighlights = () => {
itemRefs.value.forEach(element => {
if (element) {
element.style.backgroundColor = '';
element.style.fontWeight = '';
}
});
};
// 获取所有项目文本
const getAllItemText = () => {
const texts = itemRefs.value
.filter(element => element)
.map(element => element.textContent.trim());
alert(`所有项目: ${texts.join(', ')}`);
};
// 设置引用函数
const setItemRef = (el) => {
if (el) {
itemRefs.value.push(el);
}
};
// 组件挂载后
onMounted(() => {
console.log('项目引用数组:', itemRefs.value);
console.log('项目数量:', itemRefs.value.length);
});
</script>
<template>
<div>
<h3>v-for 中的模板引用</h3>
<button @click="addItem" style="margin-right: 10px;">添加项目</button>
<button @click="clearHighlights" style="margin-right: 10px;">清除高亮</button>
<button @click="getAllItemText">获取所有项目文本</button>
<ul style="margin-top: 20px;">
<li v-for="(item, index) in items"
:key="item.id"
:ref="setItemRef"
style="padding: 10px; margin: 5px 0; border: 1px solid #ccc;">
{{ item.name }}
<button @click="highlightItem(index)" style="margin-left: 10px;">高亮此项</button>
<button @click="removeItem(index)" style="margin-left: 5px;">移除</button>
</li>
</ul>
<p>项目总数: {{ items.length }}</p>
</div>
</template>
组合式函数(Composables)是使用 Composition API 封装的可复用逻辑函数。这是 Composition API 最强大的功能之一。
// useCounter.js - 计数器组合式函数
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
// 状态
const count = ref(initialValue);
// 计算属性
const doubled = computed(() => count.value * 2);
const isEven = computed(() => count.value % 2 === 0);
const isOdd = computed(() => !isEven.value);
// 方法
const increment = (step = 1) => {
count.value += step;
};
const decrement = (step = 1) => {
count.value -= step;
};
const reset = () => {
count.value = initialValue;
};
const setValue = (newValue) => {
count.value = newValue;
};
// 返回状态和方法
return {
// 状态
count,
// 计算属性
doubled,
isEven,
isOdd,
// 方法
increment,
decrement,
reset,
setValue
};
}
// 在组件中使用
<script setup>
import { useCounter } from './useCounter';
// 使用组合式函数
const counter = useCounter(10);
// 也可以解构使用
const { count, increment, reset } = useCounter(5);
</script>
<template>
<div>
<p>计数: {{ counter.count }}</p>
<p>双倍: {{ counter.doubled }}</p>
<p>是否为偶数: {{ counter.isEven ? '是' : '否' }}</p>
<button @click="counter.increment()">增加</button>
<button @click="counter.decrement()">减少</button>
<button @click="counter.reset()">重置</button>
<button @click="counter.setValue(100)">设为100</button>
</div>
</template>
// useMouse.js - 鼠标跟踪组合式函数
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
// 状态
const x = ref(0);
const y = ref(0);
// 计算属性
const position = computed(() => ({ x: x.value, y: y.value }));
// 更新鼠标位置
const update = (event) => {
x.value = event.clientX;
y.value = event.clientY;
};
// 添加事件监听
onMounted(() => {
window.addEventListener('mousemove', update);
});
// 移除事件监听
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
// 返回状态和方法
return {
x,
y,
position,
update // 也可以暴露update方法
};
}
// 增强版: 带选项的useMouse
export function useMouseWithOptions(options = {}) {
const { throttleDelay = 0 } = options;
const x = ref(0);
const y = ref(0);
let throttleTimer = null;
const update = (event) => {
if (throttleDelay > 0) {
if (throttleTimer) return;
throttleTimer = setTimeout(() => {
x.value = event.clientX;
y.value = event.clientY;
throttleTimer = null;
}, throttleDelay);
} else {
x.value = event.clientX;
y.value = event.clientY;
}
};
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
if (throttleTimer) {
clearTimeout(throttleTimer);
}
});
return { x, y };
}
// 在组件中使用
<script setup>
import { useMouse, useMouseWithOptions } from './useMouse';
// 基本用法
const { x, y } = useMouse();
// 带选项的用法
const { x: throttledX, y: throttledY } = useMouseWithOptions({
throttleDelay: 100 // 100ms节流
});
</script>
<template>
<div>
<h3>鼠标跟踪</h3>
<p>鼠标位置: X={{ x }}, Y={{ y }}</p>
<p>节流位置: X={{ throttledX }}, Y={{ throttledY }}</p>
<div style="height: 300px; border: 1px solid #ccc; margin-top: 20px;">
在此区域移动鼠标
</div>
</div>
</template>
// useFetch.js - 数据获取组合式函数
import { ref, watch, computed } from 'vue';
export function useFetch(url, options = {}) {
// 状态
const data = ref(null);
const error = ref(null);
const isLoading = ref(false);
// 配置
const {
immediate = true,
manual = false,
onSuccess,
onError,
transform
} = options;
// 计算属性
const hasError = computed(() => !!error.value);
const hasData = computed(() => !!data.value);
const isEmpty = computed(() =>
Array.isArray(data.value) ? data.value.length === 0 : !data.value
);
// 获取数据
const execute = async (executeUrl = url, executeOptions = {}) => {
isLoading.value = true;
error.value = null;
try {
const response = await fetch(executeUrl, executeOptions);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
let result = await response.json();
// 数据转换
if (transform && typeof transform === 'function') {
result = transform(result);
}
data.value = result;
// 成功回调
if (onSuccess) {
onSuccess(result);
}
return result;
} catch (err) {
error.value = err.message || '未知错误';
// 错误回调
if (onError) {
onError(err);
}
throw err;
} finally {
isLoading.value = false;
}
};
// 重新获取
const refetch = () => execute();
// 重置状态
const reset = () => {
data.value = null;
error.value = null;
isLoading.value = false;
};
// 自动获取
if (immediate && !manual) {
execute();
}
// 监听URL变化
if (typeof url === 'function' || (options.watch && typeof url !== 'function')) {
watch(
() => (typeof url === 'function' ? url() : url),
(newUrl) => {
if (newUrl) {
execute(newUrl);
}
},
{ immediate: !manual }
);
}
return {
// 状态
data,
error,
isLoading,
// 计算属性
hasError,
hasData,
isEmpty,
// 方法
execute,
refetch,
reset
};
}
// 在组件中使用
<script setup>
import { computed } from 'vue';
import { useFetch } from './useFetch';
// 基本用法
const { data: userData, isLoading, error } = useFetch(
'https://jsonplaceholder.typicode.com/users/1'
);
// 带选项的用法
const { data: posts, refetch } = useFetch(
() => `https://jsonplaceholder.typicode.com/posts?userId=${userId.value}`,
{
manual: true, // 手动执行
transform: (data) => data.slice(0, 5), // 只取前5条
onSuccess: (data) => {
console.log('数据获取成功:', data);
},
onError: (err) => {
console.error('数据获取失败:', err);
}
}
);
// 依赖其他数据的获取
const userId = ref(1);
const { data: user } = useFetch(
() => `https://jsonplaceholder.typicode.com/users/${userId.value}`,
{ watch: true } // 监听userId变化
);
const changeUser = () => {
userId.value = userId.value === 1 ? 2 : 1;
};
</script>
<template>
<div>
<h3>数据获取示例</h3>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error" class="error">错误: {{ error }}</div>
<div v-else-if="userData">
<p>用户: {{ userData.name }}</p>
<p>邮箱: {{ userData.email }}</p>
</div>
<div>
<p>当前用户ID: {{ userId }}</p>
<button @click="changeUser">切换用户</button>
<button @click="refetch">重新获取数据</button>
</div>
<div v-if="user">
<h4>用户信息</h4>
<p>{{ user.name }} - {{ user.email }}</p>
</div>
</div>
</template>
// useLocalStorage.js - 本地存储组合式函数
import { ref, watch } from 'vue';
export function useLocalStorage(key, initialValue) {
// 从localStorage读取初始值
const readValue = () => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`读取localStorage键"${key}"时出错:`, error);
return initialValue;
}
};
// 状态
const storedValue = ref(readValue());
// 写入localStorage
const setValue = (value) => {
try {
// 允许值是函数,像useState一样
const valueToStore =
typeof value === 'function' ? value(storedValue.value) : value;
// 保存状态
storedValue.value = valueToStore;
// 保存到localStorage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// 触发storage事件,让其他标签页也能响应
window.dispatchEvent(new StorageEvent('storage', { key }));
} catch (error) {
console.warn(`写入localStorage键"${key}"时出错:`, error);
}
};
// 移除
const removeValue = () => {
try {
window.localStorage.removeItem(key);
storedValue.value = initialValue;
window.dispatchEvent(new StorageEvent('storage', { key }));
} catch (error) {
console.warn(`移除localStorage键"${key}"时出错:`, error);
}
};
// 监听其他标签页的变化
const handleStorageChange = (event) => {
if (event.key === key && event.storageArea === window.localStorage) {
storedValue.value = readValue();
}
};
// 添加事件监听
window.addEventListener('storage', handleStorageChange);
// 组件卸载时清理
const cleanup = () => {
window.removeEventListener('storage', handleStorageChange);
};
return {
value: storedValue,
setValue,
removeValue,
cleanup
};
}
// useSessionStorage.js - SessionStorage版本
export function useSessionStorage(key, initialValue) {
const readValue = () => {
try {
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`读取sessionStorage键"${key}"时出错:`, error);
return initialValue;
}
};
const storedValue = ref(readValue());
const setValue = (value) => {
try {
const valueToStore =
typeof value === 'function' ? value(storedValue.value) : value;
storedValue.value = valueToStore;
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.warn(`写入sessionStorage键"${key}"时出错:`, error);
}
};
const removeValue = () => {
try {
window.sessionStorage.removeItem(key);
storedValue.value = initialValue;
} catch (error) {
console.warn(`移除sessionStorage键"${key}"时出错:`, error);
}
};
return {
value: storedValue,
setValue,
removeValue
};
}
// 在组件中使用
<script setup>
import { useLocalStorage, useSessionStorage } from './useStorage';
// 使用localStorage
const { value: theme, setValue: setTheme } = useLocalStorage('theme', 'light');
const { value: userPreferences } = useLocalStorage('userPreferences', {
notifications: true,
language: 'zh-CN',
fontSize: 14
});
// 使用sessionStorage
const { value: sessionToken, setValue: setSessionToken } =
useSessionStorage('sessionToken', null);
// 方法
const toggleTheme = () => {
setTheme(theme.value === 'light' ? 'dark' : 'light');
};
const updatePreferences = () => {
setTheme({
...userPreferences.value,
fontSize: 16,
theme: theme.value
});
};
const login = () => {
setSessionToken('mock-jwt-token-123456');
};
const logout = () => {
setSessionToken(null);
};
</script>
<template>
<div :class="theme">
<h3>本地存储示例</h3>
<p>当前主题: {{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
<div style="margin-top: 20px;">
<p>用户偏好设置:</p>
<pre>{{ userPreferences }}</pre>
<button @click="updatePreferences">更新偏好设置</button>
</div>
<div style="margin-top: 20px;">
<p>会话令牌: {{ sessionToken || '未登录' }}</p>
<button @click="login">登录</button>
<button @click="logout">登出</button>
</div>
<p style="margin-top: 20px; font-size: 12px; color: #666;">
提示: 尝试在多个标签页中打开此页面,切换主题时会同步更新。
</p>
</div>
</template>
<style>
.light {
background-color: white;
color: black;
padding: 20px;
}
.dark {
background-color: #333;
color: white;
padding: 20px;
}
</style>
useMouse、useFetch、useLocalStorage。这有助于区分普通函数和组合式函数,并使代码更易理解。
| 特性 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项类型组织(data、methods、computed等) | 按逻辑关注点组织,相关代码在一起 |
| 代码复用 | Mixin、高阶组件、渲染函数 | 组合式函数,更灵活和类型安全 |
| TypeScript 支持 | 支持,但类型推断有限 | 一流的 TypeScript 支持,更好的类型推断 |
| 学习曲线 | 相对平缓,概念较少 | 较陡峭,需要理解更多概念 |
| 灵活性 | 固定结构,灵活性较低 | 非常灵活,可以按需组织代码 |
| 打包体积 | Tree-shaking 不友好 | Tree-shaking 友好,只导入需要的函数 |
| 逻辑提取 | 提取困难,容易产生命名冲突 | 易于提取为组合式函数 |
| 可读性(简单组件) | 非常好,结构清晰 | 一般,需要适应新语法 |
| 可读性(复杂组件) | 较差,逻辑分散在各处 | 非常好,逻辑集中 |
// 计数器 + 用户信息
export default {
data() {
return {
count: 0,
user: {
name: '张三',
age: 25
}
};
},
computed: {
doubledCount() {
return this.count * 2;
},
userName() {
return this.user.name;
},
canVote() {
return this.user.age >= 18;
}
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
updateUser(newUser) {
this.user = { ...this.user, ...newUser };
}
},
watch: {
'user.age'(newAge, oldAge) {
console.log(`年龄从${oldAge}变为${newAge}`);
},
count(newCount) {
console.log(`计数变为${newCount}`);
}
},
mounted() {
console.log('组件已挂载');
this.fetchUser();
},
methods: {
fetchUser() {
// 获取用户数据
}
}
};
import { ref, reactive, computed, watch, onMounted } from 'vue';
export default {
setup() {
// ========== 计数器逻辑 ==========
const count = ref(0);
const doubledCount = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
watch(count, (newCount) => {
console.log(`计数变为${newCount}`);
});
// ========== 用户逻辑 ==========
const user = reactive({
name: '张三',
age: 25
});
const userName = computed(() => user.name);
const canVote = computed(() => user.age >= 18);
const updateUser = (newUser) => {
Object.assign(user, newUser);
};
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄从${oldAge}变为${newAge}`);
}
);
const fetchUser = async () => {
// 获取用户数据
};
// ========== 生命周期 ==========
onMounted(() => {
console.log('组件已挂载');
fetchUser();
});
return {
// 计数器
count,
doubledCount,
increment,
decrement,
// 用户
user,
userName,
canVote,
updateUser
};
}
};
Composition API 的优势:
useCounter(),用户逻辑提取为 useUser()Composition API 是 Vue 3 最重要的创新之一,它为 Vue 应用开发带来了革命性的改进:
虽然 Composition API 的学习曲线比 Options API 更陡峭,但一旦掌握,它将极大地提升你的开发效率和代码质量。对于新项目和复杂应用,Composition API 是更好的选择。