目录
- 第一章:基础概念
- [第二章:ref 和 reactive](#第二章:ref 和 reactive)
- [第三章:computed 和 watch](#第三章:computed 和 watch)
- 第四章:生命周期钩子
- [第五章:高级响应式 API](#第五章:高级响应式 API)
- 第六章:实战案例
- 第七章:最佳实践与性能优化
第一章:基础概念
1.1 什么是响应式?
响应式是 Vue 的核心特性之一,指的是当数据发生变化时,视图会自动更新。
javascript
// 传统方式:需要手动更新 DOM
let count = 0;
document.getElementById('app').innerHTML = count;
count++; // 需要再次手动更新 DOM
// Vue 响应式:自动更新
const count = ref(0);
// 当 count.value 改变时,视图自动更新
1.2 Vue3 响应式原理
Vue3 使用 Proxy 替代了 Vue2 的 Object.defineProperty:
javascript
// Vue2: Object.defineProperty(有局限性)
Object.defineProperty(obj, 'key', {
get() { return value; },
set(newVal) { value = newVal; }
});
// Vue3: Proxy(更强大)
const proxy = new Proxy(obj, {
get(target, key) { return target[key]; },
set(target, key, value) {
target[key] = value;
return true;
}
});
优势:
- ✅ 可以检测属性的添加和删除
- ✅ 可以检测数组索引和长度的变化
- ✅ 性能更好
第二章:ref 和 reactive
2.1 ref - 基础数据类型响应式
ref 用于创建基本类型(string、number、boolean)的响应式数据。
基础用法
vue
<script setup>
import { ref } from 'vue';
// 创建响应式数据
const count = ref(0);
const name = ref('张三');
const isActive = ref(true);
// 修改值(在 JS 中需要通过 .value 访问)
function increment() {
count.value++;
}
function changeName() {
name.value = '李四';
}
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>姓名:{{ name }}</p>
<button @click="increment">增加</button>
<button @click="changeName">改名</button>
</div>
</template>
注意事项
javascript
// ❌ 错误:直接赋值不会触发响应
let count = ref(0);
count = 5; // 这样会丢失响应性
// ✅ 正确:通过 .value 修改
count.value = 5;
// ✅ 模板中不需要 .value
// <p>{{ count }}</p> // 自动解包
2.2 reactive - 对象类型响应式
reactive 用于创建对象类型的响应式数据。
基础用法
vue
<script setup>
import { reactive } from 'vue';
// 创建响应式对象
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
// 修改属性(不需要 .value)
function updateAge() {
user.age = 26;
}
function addPhone() {
user.phone = '13800138000'; // 可以直接添加新属性
}
</script>
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<p>邮箱:{{ user.email }}</p>
<p v-if="user.phone">电话:{{ user.phone }}</p>
<button @click="updateAge">增加年龄</button>
<button @click="addPhone">添加电话</button>
</div>
</template>
嵌套对象
javascript
const state = reactive({
user: {
name: '张三',
address: {
city: '北京',
district: '朝阳区'
}
},
scores: [95, 87, 92]
});
// 嵌套对象也是响应式的
state.user.address.city = '上海'; // ✅ 响应式
state.scores.push(88); // ✅ 响应式
2.3 ref vs reactive 对比
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象 | 仅对象/数组 |
| 访问方式 | 需要 .value |
直接访问 |
| 模板中 | 自动解包 | 直接使用 |
| 替换整个对象 | ✅ 支持 | ❌ 会丢失响应性 |
| 推荐场景 | 基本类型、简单值 | 复杂对象 |
选择建议
javascript
// ✅ 基本类型用 ref
const count = ref(0);
const name = ref('');
// ✅ 对象用 reactive
const user = reactive({ name: '', age: 0 });
// ✅ 也可以用 ref 包裹对象(更灵活)
const user = ref({ name: '', age: 0 });
// 使用时:user.value.name
// ❌ 不要用 reactive 包裹基本类型
const count = reactive(0); // 不推荐
2.4 toRefs - 保持响应性解构
当你需要从 reactive 对象中解构属性时,使用 toRefs 保持响应性。
vue
<script setup>
import { reactive, toRefs } from 'vue';
const user = reactive({
name: '张三',
age: 25
});
// ❌ 错误:解构后失去响应性
// const { name, age } = user;
// ✅ 正确:使用 toRefs
const { name, age } = toRefs(user);
function updateName() {
name.value = '李四'; // 需要 .value
}
</script>
<template>
<div>
<p>{{ name }} - {{ age }}</p>
<button @click="updateName">改名</button>
</div>
</template>
第三章:computed 和 watch
3.1 computed - 计算属性
计算属性基于依赖自动缓存,只有依赖变化时才重新计算。
基础用法
vue
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('张');
const lastName = ref('三');
// 只读计算属性
const fullName = computed(() => {
return firstName.value + lastName.value;
});
// 可写计算属性
const fullNameWritable = computed({
get() {
return firstName.value + lastName.value;
},
set(newValue) {
// 假设新值是 "李四"
firstName.value = newValue[0];
lastName.value = newValue.slice(1);
}
});
</script>
<template>
<div>
<input v-model="firstName" placeholder="姓" />
<input v-model="lastName" placeholder="名" />
<p>全名:{{ fullName }}</p>
<p>可写全名:{{ fullNameWritable }}</p>
</div>
</template>
实际案例:购物车总价
vue
<script setup>
import { ref, computed } from 'vue';
const cart = ref([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 },
{ id: 3, name: '商品C', price: 150, quantity: 3 }
]);
// 计算总价
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
});
// 计算商品总数
const totalItems = computed(() => {
return cart.value.reduce((sum, item) => {
return sum + item.quantity;
}, 0);
});
// 是否有商品
const hasItems = computed(() => {
return cart.value.length > 0;
});
</script>
<template>
<div>
<div v-for="item in cart" :key="item.id">
<span>{{ item.name }}</span>
<span>¥{{ item.price }}</span>
<input v-model.number="item.quantity" type="number" />
</div>
<hr />
<p>商品总数:{{ totalItems }}</p>
<p>总价:¥{{ totalPrice }}</p>
<p v-if="!hasItems">购物车为空</p>
</div>
</template>
3.2 watch - 侦听器
watch 用于监听数据变化并执行副作用操作。
基础用法
vue
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const name = ref('张三');
// 监听单个 ref
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`);
});
// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}`);
console.log(`name: ${oldName} -> ${newName}`);
});
</script>
<template>
<div>
<p>{{ count }} - {{ name }}</p>
<button @click="count++">增加</button>
<button @click="name = '李四'">改名</button>
</div>
</template>
监听 reactive 对象
vue
<script setup>
import { reactive, watch } from 'vue';
const user = reactive({
name: '张三',
age: 25
});
// 监听整个对象(需要深度监听)
watch(
() => ({ ...user }), // 返回新对象以触发监听
(newValue, oldValue) => {
console.log('用户信息变化:', newValue);
}
);
// 或者监听特定属性
watch(
() => user.name,
(newName, oldName) => {
console.log(`姓名变化: ${oldName} -> ${newName}`);
}
);
</script>
watch 选项
javascript
watch(
source,
callback,
{
immediate: true, // 立即执行一次
deep: true, // 深度监听
flush: 'post' // 'pre' | 'post' | 'sync'
}
);
实际案例:搜索防抖
vue
<script setup>
import { ref, watch } from 'vue';
const searchQuery = ref('');
const searchResults = ref([]);
let timeoutId = null;
// 防抖搜索
watch(searchQuery, (newQuery) => {
// 清除之前的定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
// 设置新的定时器
timeoutId = setTimeout(async () => {
if (newQuery.trim()) {
const response = await fetch(`/api/search?q=${newQuery}`);
searchResults.value = await response.json();
} else {
searchResults.value = [];
}
}, 300); // 300ms 防抖
});
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="搜索..." />
<ul>
<li v-for="result in searchResults" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
3.3 watchEffect - 自动追踪依赖
watchEffect 会自动追踪回调中使用的响应式依赖。
vue
<script setup>
import { ref, watchEffect } from 'vue';
const userId = ref(1);
const user = ref(null);
// 自动追踪 userId 的变化
watchEffect(async () => {
console.log('用户ID:', userId.value);
const response = await fetch(`/api/users/${userId.value}`);
user.value = await response.json();
});
// 当 userId 改变时,watchEffect 会自动重新执行
function changeUser() {
userId.value = 2;
}
</script>
<template>
<div>
<p v-if="user">{{ user.name }}</p>
<button @click="changeUser">切换用户</button>
</div>
</template>
3.4 computed vs watch 对比
| 特性 | computed | watch |
|---|---|---|
| 返回值 | ✅ 有返回值 | ❌ 无返回值 |
| 缓存 | ✅ 自动缓存 | ❌ 不缓存 |
| 异步操作 | ❌ 不支持 | ✅ 支持 |
| 副作用 | ❌ 不应该有 | ✅ 专门处理 |
| 使用场景 | 派生状态 | 异步操作、副作用 |
第四章:生命周期钩子
4.1 生命周期概览
setup()
↓
onBeforeMount
↓
onMounted
↓
onBeforeUpdate
↓
onUpdated
↓
onBeforeUnmount
↓
onUnmounted
4.2 常用生命周期
vue
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
const count = ref(0);
// 组件挂载前
onBeforeMount(() => {
console.log('组件即将挂载');
});
// 组件挂载后(常用于初始化、请求数据)
onMounted(() => {
console.log('组件已挂载');
fetchData();
});
// 数据更新前
onBeforeUpdate(() => {
console.log('组件即将更新');
});
// 数据更新后
onUpdated(() => {
console.log('组件已更新');
});
// 组件卸载前
onBeforeUnmount(() => {
console.log('组件即将卸载');
});
// 组件卸载后(清理工作)
onUnmounted(() => {
console.log('组件已卸载');
clearInterval(timerId);
});
async function fetchData() {
const response = await fetch('/api/data');
count.value = await response.json();
}
let timerId = setInterval(() => {
count.value++;
}, 1000);
</script>
<template>
<div>
<p>{{ count }}</p>
</div>
</template>
4.3 生命周期与浏览器操作的对应关系
完整生命周期流程图
页面加载/路由进入
↓
setup() - 组件实例创建
↓
onBeforeMount - DOM 还未渲染
↓
【浏览器渲染 DOM】
↓
onMounted - DOM 已渲染完成 ✅ 可操作 DOM
↓
━━━━━━━━━━━━━━━━━━━━━━━
用户交互/数据变化
↓
onBeforeUpdate - 数据变化,DOM 更新前
↓
【浏览器更新 DOM】
↓
onUpdated - DOM 更新完成
↓
━━━━━━━━━━━━━━━━━━━━━━━
页面关闭/路由离开
↓
onBeforeUnmount - 组件销毁前
↓
【浏览器移除 DOM】
↓
onUnmounted - 组件已销毁 ✅ 清理工作
各生命周期触发的浏览器操作
| 生命周期 | 触发的浏览器操作 | 典型使用场景 |
|---|---|---|
| onBeforeMount | 组件即将插入 DOM | 服务端渲染(SSR)相关逻辑 |
| onMounted | ✅ 页面首次加载 ✅ 路由导航进入 ✅ 条件渲染 v-if="true" |
- 发起 API 请求 - 操作 DOM 元素 - 初始化第三方库 - 添加事件监听 |
| onBeforeUpdate | 响应式数据变化后,DOM 更新前 | - 获取更新前的 DOM 状态 - 性能优化 |
| onUpdated | DOM 更新完成后 | - 操作更新后的 DOM - 同步外部库状态 |
| onBeforeUnmount | ❌ 路由离开 ❌ v-if="false" ❌ 组件销毁 |
- 保存临时数据 - 取消未完成的请求 |
| onUnmounted | ✅ 页面关闭/刷新 ✅ 路由导航离开 ✅ 条件渲染 v-if="false" |
- 清除定时器 - 移除事件监听 - 断开 WebSocket - 清理资源 |
实际示例:不同场景的生命周期执行
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const showComponent = ref(true);
// 场景1:页面加载时执行
onMounted(() => {
console.log('✅ 页面加载完成');
// 这里会执行:
// - 首次访问该路由
// - 刷新页面
// - 从其他路由导航过来
});
// 场景2:路由离开时执行
onUnmounted(() => {
console.log('✅ 离开页面');
// 这里会执行:
// - 点击链接跳转到其他路由
// - 调用 router.push()/router.back()
// - 浏览器后退/前进按钮
});
// 场景3:条件渲染
function toggleComponent() {
showComponent.value = !showComponent.value;
// showComponent 变为 false 时触发 onUnmounted
// showComponent 变为 true 时重新触发 onMounted
}
// 场景4:页面刷新/关闭
window.addEventListener('beforeunload', () => {
console.log('⚠️ 页面即将关闭或刷新');
// 注意:这里不能执行异步操作
});
</script>
<template>
<div>
<button @click="toggleComponent">切换组件显示</button>
<button @click="router.push('/other')">跳转到其他页面</button>
<ChildComponent v-if="showComponent" />
</div>
</template>
4.4 Keep-Alive 对生命周期的影响
什么是 Keep-Alive?
<keep-alive> 是 Vue 的内置组件,用于缓存不活动的组件实例,而不是销毁它们。
Keep-Alive 新增的生命周期钩子
vue
<script setup>
import {
onMounted,
onUnmounted,
onActivated, // ✅ keep-alive 专属
onDeactivated // ✅ keep-alive 专属
} from 'vue';
// 组件首次创建时执行(只执行一次)
onMounted(() => {
console.log('✅ mounted - 组件首次挂载');
});
// 组件被激活时执行(每次显示都执行)
onActivated(() => {
console.log('✅ activated - 组件被激活/显示');
// 相当于 "可见" 时的回调
});
// 组件被停用时执行(每次隐藏都执行)
onDeactivated(() => {
console.log('✅ deactivated - 组件被停用/隐藏');
// 相当于 "不可见" 时的回调
});
// 组件真正销毁时执行(keep-alive 中不会执行)
onUnmounted(() => {
console.log('❌ unmounted - 组件真正销毁');
// 在 keep-alive 中,这个钩子不会被调用
});
</script>
Keep-Alive 生命周期执行顺序
不使用 Keep-Alive:
进入页面: setup → onBeforeMount → onMounted
离开页面: onBeforeUnmount → onUnmounted
再次进入: setup → onBeforeMount → onMounted (重新创建)
使用 Keep-Alive:
首次进入: setup → onBeforeMount → onMounted → onActivated
切换到其他页面: onDeactivated (缓存,不销毁)
再次返回: onActivated (从缓存恢复,不重新创建)
最终离开(如关闭标签): onDeactivated → onUnmounted
实际案例:Tab 切换场景
vue
<!-- App.vue 或父组件 -->
<template>
<div>
<nav>
<button @click="currentTab = 'Home'">首页</button>
<button @click="currentTab = 'About'">关于</button>
<button @click="currentTab = 'Contact'">联系</button>
</nav>
<!-- 使用 keep-alive 缓存组件 -->
<keep-alive>
<component :is="currentTab" />
</keep-alive>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Home from './components/Home.vue';
import About from './components/About.vue';
import Contact from './components/Contact.vue';
const currentTab = ref('Home');
</script>
vue
<!-- components/Home.vue -->
<script setup>
import { ref, onMounted, onActivated, onDeactivated, onUnmounted } from 'vue';
const scrollPosition = ref(0);
let timerId = null;
console.log('📝 Home 组件脚本执行');
// 只在首次创建时执行一次
onMounted(() => {
console.log('🏠 Home mounted - 首次加载');
// 初始化:只执行一次
loadInitialData();
});
// 每次显示 Tab 时执行
onActivated(() => {
console.log('🏠 Home activated - 切换到首页');
// 恢复滚动位置
window.scrollTo(0, scrollPosition.value);
// 启动定时器
timerId = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 刷新数据(可选)
refreshData();
});
// 每次隐藏 Tab 时执行
onDeactivated(() => {
console.log('🏠 Home deactivated - 离开首页');
// 保存滚动位置
scrollPosition.value = window.scrollY;
// 暂停定时器(节省资源)
if (timerId) {
clearInterval(timerId);
timerId = null;
}
});
// keep-alive 中不会执行,除非组件真正被销毁
onUnmounted(() => {
console.log('🏠 Home unmounted - 真正销毁');
// 清理工作
});
async function loadInitialData() {
console.log('加载初始数据...');
}
async function refreshData() {
console.log('刷新数据...');
}
</script>
<template>
<div class="home">
<h1>首页</h1>
<p>滚动位置:{{ scrollPosition }}</p>
<div style="height: 2000px; background: linear-gradient(to bottom, #fff, #eee);">
向下滚动测试...
</div>
</div>
</template>
Keep-Alive 的高级用法
1. 包含/排除特定组件
vue
<template>
<div>
<!-- 只缓存 Home 和 About -->
<keep-alive include="Home,About">
<component :is="currentTab" />
</keep-alive>
<!-- 除了 Contact 都缓存 -->
<keep-alive exclude="Contact">
<component :is="currentTab" />
</keep-alive>
<!-- 最多缓存 5 个组件实例 -->
<keep-alive :max="5">
<component :is="currentTab" />
</keep-alive>
</div>
</template>
2. 路由中的 Keep-Alive
javascript
// router/index.js
const routes = [
{
path: '/home',
name: 'Home',
component: Home,
meta: { keepAlive: true } // 需要缓存
},
{
path: '/about',
name: 'About',
component: About,
meta: { keepAlive: false } // 不缓存
}
];
vue
<!-- App.vue -->
<template>
<router-view v-slot="{ Component }">
<!-- 缓存设置了 keepAlive: true 的路由 -->
<keep-alive>
<component
:is="Component"
v-if="$route.meta.keepAlive"
/>
</keep-alive>
<!-- 不缓存其他路由 -->
<component
:is="Component"
v-if="!$route.meta.keepAlive"
/>
</router-view>
</template>
3. 强制刷新缓存组件
vue
<script setup>
import { ref, nextTick } from 'vue';
const isRouterAlive = ref(true);
// 强制刷新当前路由组件
function forceRefresh() {
isRouterAlive.value = false;
nextTick(() => {
isRouterAlive.value = true;
});
}
</script>
<template>
<div>
<button @click="forceRefresh">强制刷新</button>
<router-view v-if="isRouterAlive" v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
Keep-Alive 生命周期对比表
| 场景 | onMounted | onActivated | onDeactivated | onUnmounted |
|---|---|---|---|---|
| 首次进入 | ✅ | ✅ | ❌ | ❌ |
| 切换到其他 Tab | ❌ | ❌ | ✅ | ❌ |
| 再次返回 | ❌ | ✅ | ❌ | ❌ |
| 第 N 次切换 | ❌ | ✅ | ✅ | ❌ |
| 真正销毁 | ❌ | ❌ | ✅ | ✅ |
Keep-Alive 使用建议
javascript
// ✅ 适合使用 Keep-Alive 的场景
// 1. Tab 切换,保持表单输入状态
// 2. 列表页 → 详情页 → 返回列表页(保持滚动位置和筛选条件)
// 3. 频繁切换的组件,避免重复创建开销
// ❌ 不适合使用 Keep-Alive 的场景
// 1. 组件数据需要每次都最新(如实时数据)
// 2. 组件占用大量内存
// 3. 组件有严格的初始化逻辑
// 💡 最佳实践
onActivated(() => {
// 1. 恢复 UI 状态(滚动位置、表单数据等)
// 2. 可选择性地刷新数据
// 3. 重启定时器、动画等
});
onDeactivated(() => {
// 1. 保存 UI 状态
// 2. 暂停不必要的操作(定时器、轮询等)
// 3. 不要在这里清理资源(组件还会复用)
});
onUnmounted(() => {
// 真正的清理工作放在这里
// 即使使用了 keep-alive,最终销毁时也会执行
});
4.5 实际案例:定时器和事件监听清理
在实际开发中,我们经常需要在组件挂载时添加事件监听器、启动定时器,并在组件卸载时清理它们,以避免内存泄漏。
基础示例:滚动监听和定时器
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const scrollPosition = ref(0);
const currentTime = ref('');
let timerId = null;
onMounted(() => {
console.log('✅ 组件已挂载,开始监听...');
// 1. 添加滚动事件监听
window.addEventListener('scroll', handleScroll);
// 2. 启动定时器,每秒更新时间
timerId = setInterval(() => {
currentTime.value = new Date().toLocaleTimeString();
console.log('⏰ 定时器执行:', currentTime.value);
}, 1000);
});
onUnmounted(() => {
console.log('❌ 组件即将卸载,清理资源...');
// 1. 移除滚动事件监听(必须!)
window.removeEventListener('scroll', handleScroll);
// 2. 清除定时器(必须!)
if (timerId) {
clearInterval(timerId);
timerId = null;
console.log('✅ 定时器已清除');
}
});
function handleScroll() {
scrollPosition.value = window.scrollY;
}
</script>
<template>
<div>
<p>当前滚动位置:{{ scrollPosition }}px</p>
<p>当前时间:{{ currentTime }}</p>
<div style="height: 2000px; background: linear-gradient(to bottom, #fff, #eee);">
向下滚动查看位置变化...
</div>
</div>
</template>
进阶示例:多个资源的管理
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const messages = ref([]);
let eventListeners = [];
let timers = [];
let observers = [];
onMounted(() => {
// 1. 添加多个事件监听
const resizeHandler = () => console.log('窗口大小变化');
const keydownHandler = (e) => console.log('按键:', e.key);
window.addEventListener('resize', resizeHandler);
window.addEventListener('keydown', keydownHandler);
// 记录事件监听器以便清理
eventListeners.push(
{ target: window, event: 'resize', handler: resizeHandler },
{ target: window, event: 'keydown', handler: keydownHandler }
);
// 2. 启动多个定时器
const timer1 = setInterval(() => {
console.log('定时器1:数据轮询');
fetchData();
}, 5000);
const timer2 = setTimeout(() => {
console.log('定时器2:延迟操作');
}, 10000);
timers.push(timer1, timer2);
// 3. 创建 IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素可见:', entry.target);
}
});
});
const element = document.querySelector('.target-element');
if (element) {
observer.observe(element);
observers.push(observer);
}
// 4. WebSocket 连接
const ws = new WebSocket('ws://example.com');
ws.onmessage = (event) => {
messages.value.push(event.data);
};
timers.push({ type: 'websocket', instance: ws });
});
onUnmounted(() => {
console.log('🧹 开始清理所有资源...');
// 1. 移除所有事件监听
eventListeners.forEach(({ target, event, handler }) => {
target.removeEventListener(event, handler);
});
eventListeners = [];
// 2. 清除所有定时器
timers.forEach(timer => {
if (timer.type === 'websocket') {
timer.instance.close();
console.log('✅ WebSocket 已关闭');
} else if (typeof timer === 'number') {
clearInterval(timer);
clearTimeout(timer);
}
});
timers = [];
// 3. 断开所有观察者
observers.forEach(observer => {
observer.disconnect();
});
observers = [];
console.log('✅ 所有资源已清理');
});
async function fetchData() {
// 模拟数据获取
console.log('获取数据...');
}
</script>
<template>
<div>
<h2>消息列表</h2>
<ul>
<li v-for="(msg, index) in messages" :key="index">
{{ msg }}
</li>
</ul>
<div class="target-element">观察目标元素</div>
</div>
</template>
封装清理工具函数
为了简化资源管理,可以封装一个通用的清理工具:
javascript
// composables/useCleanup.js
import { onUnmounted } from 'vue';
export function useCleanup() {
const cleanupFns = [];
// 注册清理函数
function addCleanup(fn) {
cleanupFns.push(fn);
}
// 在组件卸载时执行所有清理函数
onUnmounted(() => {
console.log(`🧹 执行 ${cleanupFns.length} 个清理函数`);
cleanupFns.forEach(fn => {
try {
fn();
} catch (error) {
console.error('清理函数执行失败:', error);
}
});
cleanupFns.length = 0; // 清空数组
});
return { addCleanup };
}
vue
<!-- 使用清理工具 -->
<script setup>
import { ref, onMounted } from 'vue';
import { useCleanup } from './composables/useCleanup';
const { addCleanup } = useCleanup();
const data = ref(null);
onMounted(() => {
// 1. 添加事件监听
const handler = () => console.log('窗口调整');
window.addEventListener('resize', handler);
addCleanup(() => {
window.removeEventListener('resize', handler);
console.log('✅ 移除 resize 监听');
});
// 2. 启动定时器
const timerId = setInterval(() => {
console.log('定时任务');
}, 1000);
addCleanup(() => {
clearInterval(timerId);
console.log('✅ 清除定时器');
});
// 3. 创建观察者
const observer = new IntersectionObserver(() => {});
observer.observe(document.body);
addCleanup(() => {
observer.disconnect();
console.log('✅ 断开观察者');
});
// 4. API 请求(可取消)
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(result => {
data.value = result;
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('请求失败:', err);
}
});
addCleanup(() => {
controller.abort();
console.log('✅ 取消 API 请求');
});
});
</script>
<template>
<div>
<h2>数据展示</h2>
<pre>{{ data }}</pre>
</div>
</template>
常见需要清理的资源清单
| 资源类型 | 创建方式 | 清理方式 | 不清理的后果 |
|---|---|---|---|
| 事件监听器 | addEventListener |
removeEventListener |
内存泄漏、意外触发 |
| 定时器 | setInterval/setTimeout |
clearInterval/clearTimeout |
持续执行、内存泄漏 |
| IntersectionObserver | new IntersectionObserver |
observer.disconnect() |
内存泄漏 |
| ResizeObserver | new ResizeObserver |
observer.disconnect() |
内存泄漏 |
| MutationObserver | new MutationObserver |
observer.disconnect() |
内存泄漏 |
| WebSocket | new WebSocket |
ws.close() |
连接泄漏 |
| EventEmitter | emitter.on() |
emitter.off() |
内存泄漏、重复触发 |
| 第三方库实例 | 如 Chart.js、Mapbox | 库提供的 destroy/dispose 方法 | 内存泄漏 |
| 订阅 | RxJS subscribe | subscription.unsubscribe() |
内存泄漏 |
| 动画帧 | requestAnimationFrame |
cancelAnimationFrame |
持续执行 |
注意事项和最佳实践
javascript
// ✅ 最佳实践 1:总是成对出现
onMounted(() => {
window.addEventListener('scroll', handler);
});
onUnmounted(() => {
window.removeEventListener('scroll', handler);
});
// ❌ 错误做法:只在 mounted 中添加,忘记清理
onMounted(() => {
window.addEventListener('scroll', handler);
// 没有对应的 removeEventListener
});
// ✅ 最佳实践 2:保存引用以便清理
let timerId = null;
onMounted(() => {
timerId = setInterval(() => {}, 1000);
});
onUnmounted(() => {
if (timerId) {
clearInterval(timerId);
}
});
// ❌ 错误做法:无法清理(没有保存引用)
onMounted(() => {
setInterval(() => {}, 1000); // 无法清除!
});
// ✅ 最佳实践 3:处理异步清理
let abortController = null;
onMounted(async () => {
abortController = new AbortController();
try {
const response = await fetch('/api/data', {
signal: abortController.signal
});
// 处理数据...
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
});
onUnmounted(() => {
if (abortController) {
abortController.abort(); // 取消未完成的请求
}
});
// ✅ 最佳实践 4:使用 try-catch 保护清理逻辑
onUnmounted(() => {
try {
// 清理代码
clearInterval(timerId);
observer.disconnect();
} catch (error) {
console.error('清理失败:', error);
}
});
Keep-Alive 场景下的特殊处理
当组件被 <keep-alive> 包裹时,onUnmounted 不会在切换时执行,需要使用 onDeactivated:
vue
<script setup>
import { ref, onMounted, onActivated, onDeactivated, onUnmounted } from 'vue';
let timerId = null;
onMounted(() => {
console.log('首次挂载');
});
onActivated(() => {
console.log('组件激活 - 重启定时器');
timerId = setInterval(() => {
console.log('定时器运行');
}, 1000);
});
onDeactivated(() => {
console.log('组件停用 - 暂停定时器');
if (timerId) {
clearInterval(timerId);
timerId = null;
}
});
onUnmounted(() => {
console.log('真正销毁 - 最终清理');
// 这里做最终的清理工作
if (timerId) {
clearInterval(timerId);
}
});
</script>
关键点:
- 🔸 在
onActivated中重启定时器、恢复监听 - 🔸 在
onDeactivated中暂停定时器、暂时移除监听 - 🔸 在
onUnmounted中做最终的彻底清理
第五章:高级响应式 API
5.1 shallowRef 和 shallowReactive
浅层响应式,只监听第一层的变化。
vue
<script setup>
import { ref, shallowRef, reactive, shallowReactive } from 'vue';
// 普通 ref - 深层响应
const normalRef = ref({ nested: { value: 1 } });
normalRef.value.nested.value = 2; // ✅ 触发更新
// shallowRef - 只监听 .value 的替换
const shallow = shallowRef({ nested: { value: 1 } });
shallow.value.nested.value = 2; // ❌ 不触发更新
shallow.value = { nested: { value: 2 } }; // ✅ 触发更新
// shallowReactive - 只监听第一层
const shallowObj = shallowReactive({
name: '张三',
profile: { age: 25 }
});
shallowObj.name = '李四'; // ✅ 触发更新
shallowObj.profile.age = 26; // ❌ 不触发更新
</script>
使用场景: 大型不可变数据结构、性能优化
5.2 readonly 和 shallowReadonly
创建只读代理,防止数据被修改。
vue
<script setup>
import { reactive, readonly, shallowReadonly } from 'vue';
const original = reactive({ count: 0 });
// 完全只读
const readOnlyCopy = readonly(original);
readOnlyCopy.count = 1; // ⚠️ 警告:无法修改只读属性
// 浅层只读
const shallowReadOnlyCopy = shallowReadonly({
name: '张三',
profile: { age: 25 }
});
shallowReadOnlyCopy.name = '李四'; // ⚠️ 警告
shallowReadOnlyCopy.profile.age = 26; // ✅ 可以修改(深层不是只读)
</script>
使用场景: 保护配置对象、防止子组件修改 props
5.3 triggerRef 和 shallowReactive
手动触发 shallowRef 的更新。
vue
<script setup>
import { shallowRef, triggerRef } from 'vue';
const state = shallowRef({ count: 0 });
function mutate() {
// 修改嵌套属性
state.value.count++;
// 手动触发更新
triggerRef(state);
}
</script>
5.4 customRef - 自定义 ref
创建具有自定义依赖跟踪和更新触发的 ref。
vue
<script setup>
import { customRef } from 'vue';
// 防抖 ref
function useDebouncedRef(value, delay = 300) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track(); // 追踪依赖
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // 触发更新
}, delay);
}
};
});
}
const searchText = useDebouncedRef('', 300);
</script>
<template>
<input v-model="searchText" placeholder="输入搜索..." />
<p>搜索内容:{{ searchText }}</p>
</template>
5.5 toRef 和 toValue
vue
<script setup>
import { reactive, toRef, toValue } from 'vue';
const user = reactive({ name: '张三', age: 25 });
// toRef - 为对象属性创建 ref
const nameRef = toRef(user, 'name');
nameRef.value = '李四'; // 同时修改 user.name
// toValue - 统一获取值(处理 ref 和普通值)
const value1 = toValue(nameRef); // '李四'
const value2 = toValue('普通字符串'); // '普通字符串'
</script>
第六章:实战案例
6.1 案例一:Todo List
vue
<script setup>
import { ref, computed } from 'vue';
const todos = ref([
{ id: 1, text: '学习 Vue3', completed: false },
{ id: 2, text: '编写代码', completed: true }
]);
const newTodo = ref('');
const filter = ref('all'); // all, active, completed
// 添加待办
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
});
newTodo.value = '';
}
}
// 删除待办
function removeTodo(id) {
const index = todos.value.findIndex(t => t.id === id);
if (index !== -1) {
todos.value.splice(index, 1);
}
}
// 切换完成状态
function toggleTodo(todo) {
todo.completed = !todo.completed;
}
// 过滤后的待办列表
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed);
case 'completed':
return todos.value.filter(t => t.completed);
default:
return todos.value;
}
});
// 统计信息
const stats = computed(() => ({
total: todos.value.length,
active: todos.value.filter(t => !t.completed).length,
completed: todos.value.filter(t => t.completed).length
}));
</script>
<template>
<div class="todo-app">
<h1>Todo List</h1>
<!-- 添加待办 -->
<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新待办..."
/>
<button @click="addTodo">添加</button>
</div>
<!-- 过滤器 -->
<div class="filters">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>全部</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>进行中</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>已完成</button>
</div>
<!-- 待办列表 -->
<ul class="todo-list">
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo)"
/>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计信息 -->
<div class="stats">
<p>总计:{{ stats.total }}</p>
<p>进行中:{{ stats.active }}</p>
<p>已完成:{{ stats.completed }}</p>
</div>
</div>
</template>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.add-todo {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-todo input {
flex: 1;
padding: 8px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button.active {
background-color: #42b983;
color: white;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.completed {
text-decoration: line-through;
color: #999;
}
.stats {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #eee;
}
</style>
6.2 案例二:表单验证
vue
<script setup>
import { ref, reactive, computed, watch } from 'vue';
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const errors = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const isSubmitting = ref(false);
const submitSuccess = ref(false);
// 验证规则
const validators = {
username: (value) => {
if (!value) return '用户名不能为空';
if (value.length < 3) return '用户名至少3个字符';
if (value.length > 20) return '用户名最多20个字符';
return '';
},
email: (value) => {
if (!value) return '邮箱不能为空';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) return '邮箱格式不正确';
return '';
},
password: (value) => {
if (!value) return '密码不能为空';
if (value.length < 6) return '密码至少6个字符';
if (!/[A-Z]/.test(value)) return '密码必须包含大写字母';
if (!/[0-9]/.test(value)) return '密码必须包含数字';
return '';
},
confirmPassword: (value) => {
if (!value) return '请确认密码';
if (value !== form.password) return '两次密码不一致';
return '';
}
};
// 实时验证
Object.keys(validators).forEach(field => {
watch(
() => form[field],
(value) => {
errors[field] = validators[field](value);
}
);
});
// 表单是否有效
const isValid = computed(() => {
return Object.values(errors).every(error => !error) &&
Object.values(form).every(value => value);
});
// 提交表单
async function handleSubmit() {
// 验证所有字段
Object.keys(validators).forEach(field => {
errors[field] = validators[field](form[field]);
});
if (!isValid.value) return;
isSubmitting.value = true;
try {
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('提交数据:', form);
submitSuccess.value = true;
// 重置表单
setTimeout(() => {
Object.keys(form).forEach(key => form[key] = '');
submitSuccess.value = false;
}, 2000);
} catch (error) {
console.error('提交失败:', error);
} finally {
isSubmitting.value = false;
}
}
</script>
<template>
<div class="form-container">
<h2>用户注册</h2>
<form @submit.prevent="handleSubmit">
<!-- 用户名 -->
<div class="form-group">
<label>用户名</label>
<input
v-model="form.username"
type="text"
:class="{ error: errors.username }"
/>
<span v-if="errors.username" class="error-msg">
{{ errors.username }}
</span>
</div>
<!-- 邮箱 -->
<div class="form-group">
<label>邮箱</label>
<input
v-model="form.email"
type="email"
:class="{ error: errors.email }"
/>
<span v-if="errors.email" class="error-msg">
{{ errors.email }}
</span>
</div>
<!-- 密码 -->
<div class="form-group">
<label>密码</label>
<input
v-model="form.password"
type="password"
:class="{ error: errors.password }"
/>
<span v-if="errors.password" class="error-msg">
{{ errors.password }}
</span>
</div>
<!-- 确认密码 -->
<div class="form-group">
<label>确认密码</label>
<input
v-model="form.confirmPassword"
type="password"
:class="{ error: errors.confirmPassword }"
/>
<span v-if="errors.confirmPassword" class="error-msg">
{{ errors.confirmPassword }}
</span>
</div>
<!-- 提交按钮 -->
<button
type="submit"
:disabled="!isValid || isSubmitting"
class="submit-btn"
>
{{ isSubmitting ? '提交中...' : '注册' }}
</button>
<!-- 成功提示 -->
<div v-if="submitSuccess" class="success-msg">
注册成功!
</div>
</form>
</div>
</template>
<style scoped>
.form-container {
max-width: 400px;
margin: 50px auto;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-group input.error {
border-color: #ff4444;
}
.error-msg {
color: #ff4444;
font-size: 12px;
margin-top: 5px;
display: block;
}
.submit-btn {
width: 100%;
padding: 12px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.submit-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.success-msg {
margin-top: 20px;
padding: 10px;
background-color: #d4edda;
color: #155724;
border-radius: 4px;
text-align: center;
}
</style>
6.3 案例三:组合式函数(Composable)
创建可复用的逻辑封装。
javascript
// composables/useFetch.js
import { ref, watch } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
// 自动执行
execute();
return {
data,
error,
loading,
refetch: execute
};
}
javascript
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值
const storedValue = localStorage.getItem(key);
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue);
// 监听变化,同步到 localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
vue
<!-- 使用组合式函数 -->
<script setup>
import { useFetch } from './composables/useFetch';
import { useLocalStorage } from './composables/useLocalStorage';
// 使用 useFetch
const { data: users, error, loading, refetch } = useFetch('/api/users');
// 使用 useLocalStorage
const theme = useLocalStorage('theme', 'light');
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
</script>
<template>
<div :class="theme">
<button @click="toggleTheme">
切换到{{ theme === 'light' ? '深色' : '浅色' }}模式
</button>
<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>
</div>
</template>
第七章:最佳实践与性能优化
7.1 选择 ref 还是 reactive?
javascript
// ✅ 推荐:大部分情况使用 ref
const count = ref(0);
const user = ref({ name: '', age: 0 });
// ✅ 场景:对象属性很多且经常一起使用
const state = reactive({
user: { name: '', age: 0 },
settings: { theme: 'light', lang: 'zh' }
});
// ❌ 避免:用 reactive 存储基本类型
const count = reactive(0); // 不推荐
7.2 避免常见的响应式陷阱
javascript
// ❌ 陷阱1:解构 reactive 对象
const user = reactive({ name: '张三', age: 25 });
const { name, age } = user; // 失去响应性
// ✅ 解决:使用 toRefs
const { name, age } = toRefs(user);
// ❌ 陷阱2:直接替换 reactive 对象
let user = reactive({ name: '张三' });
user = reactive({ name: '李四' }); // 失去响应性
// ✅ 解决:使用 ref 或 Object.assign
const user = ref({ name: '张三' });
user.value = { name: '李四' }; // ✅
// 或者
const user = reactive({ name: '张三' });
Object.assign(user, { name: '李四' }); // ✅
// ❌ 陷阱3:数组索引直接赋值(Vue3 已支持,但要注意)
const arr = ref([1, 2, 3]);
arr.value[0] = 10; // ✅ Vue3 支持
// ❌ 陷阱4:忘记 .value
const count = ref(0);
count++; // ❌ 错误
count.value++; // ✅ 正确
7.3 性能优化技巧
1. 使用 shallowRef 处理大型数据
javascript
import { shallowRef } from 'vue';
// 大型不可变数据
const largeData = shallowRef(getLargeDataset());
// 只在需要时替换整个对象
function updateData() {
largeData.value = getNewDataset();
}
2. 合理使用 computed 缓存
javascript
// ✅ computed 会缓存结果
const expensiveValue = computed(() => {
return heavyCalculation(data.value);
});
// ❌ 每次渲染都重新计算
const expensiveValue = () => {
return heavyCalculation(data.value);
};
3. 避免不必要的 watch
javascript
// ❌ 过度使用 watch
watch(data, () => {
updateUI();
});
// ✅ 使用 computed 或直接在模板中计算
const formattedData = computed(() => {
return formatData(data.value);
});
4. 及时清理副作用
javascript
import { onUnmounted } from 'vue';
let timerId = null;
let observer = null;
onMounted(() => {
timerId = setInterval(() => {}, 1000);
observer = new IntersectionObserver(() => {});
});
onUnmounted(() => {
// 清理所有副作用
clearInterval(timerId);
observer?.disconnect();
});
7.4 代码组织最佳实践
按功能组织代码
vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
// ===== 状态定义 =====
const searchQuery = ref('');
const users = ref([]);
const loading = ref(false);
// ===== 计算属性 =====
const filteredUsers = computed(() => {
return users.value.filter(user =>
user.name.includes(searchQuery.value)
);
});
// ===== 方法 =====
async function fetchUsers() {
loading.value = true;
try {
const response = await fetch('/api/users');
users.value = await response.json();
} finally {
loading.value = false;
}
}
// ===== 监听器 =====
watch(searchQuery, () => {
console.log('搜索条件变化:', searchQuery.value);
});
// ===== 生命周期 =====
onMounted(() => {
fetchUsers();
});
</script>
提取组合式函数
javascript
// composables/useUsers.js
import { ref, computed } from 'vue';
export function useUsers() {
const users = ref([]);
const loading = ref(false);
const searchQuery = ref('');
const filteredUsers = computed(() => {
return users.value.filter(user =>
user.name.includes(searchQuery.value)
);
});
async function fetchUsers() {
loading.value = true;
try {
const response = await fetch('/api/users');
users.value = await response.json();
} finally {
loading.value = false;
}
}
return {
users,
loading,
searchQuery,
filteredUsers,
fetchUsers
};
}
vue
<script setup>
import { useUsers } from './composables/useUsers';
const {
users,
loading,
searchQuery,
filteredUsers,
fetchUsers
} = useUsers();
onMounted(() => {
fetchUsers();
});
</script>
7.5 调试技巧
javascript
import { watchEffect } from 'vue';
// 调试响应式数据变化
watchEffect(() => {
console.log('当前状态:', {
count: count.value,
user: user.value
});
});
// Vue Devtools
// 安装 Vue Devtools 浏览器扩展
// 可以直观查看组件树、状态、事件等
总结
核心知识点回顾
-
ref vs reactive
- ref:基本类型,需要
.value - reactive:对象类型,直接访问
- ref:基本类型,需要
-
computed vs watch
- computed:派生状态,有缓存
- watch:副作用,支持异步
-
生命周期
- onMounted:初始化
- onUnmounted:清理
-
高级 API
- shallowRef/shallowReactive:性能优化
- readonly:数据保护
- customRef:自定义行为
学习路线建议
入门 → 掌握 ref/reactive/computed/watch
↓
进阶 → 理解响应式原理、生命周期
↓
高级 → 组合式函数、性能优化
↓
精通 → 源码阅读、自定义响应式系统
下一步学习
- 📚 Vue Router:路由管理
- 📚 Pinia:状态管理
- 📚 VueUse:实用组合式函数库
- 📚 Vue 源码:深入理解响应式原理