Vue3 响应式 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 浏览器扩展
// 可以直观查看组件树、状态、事件等

总结

核心知识点回顾

  1. ref vs reactive

    • ref:基本类型,需要 .value
    • reactive:对象类型,直接访问
  2. computed vs watch

    • computed:派生状态,有缓存
    • watch:副作用,支持异步
  3. 生命周期

    • onMounted:初始化
    • onUnmounted:清理
  4. 高级 API

    • shallowRef/shallowReactive:性能优化
    • readonly:数据保护
    • customRef:自定义行为

学习路线建议

复制代码
入门 → 掌握 ref/reactive/computed/watch
  ↓
进阶 → 理解响应式原理、生命周期
  ↓
高级 → 组合式函数、性能优化
  ↓
精通 → 源码阅读、自定义响应式系统

下一步学习

  • 📚 Vue Router:路由管理
  • 📚 Pinia:状态管理
  • 📚 VueUse:实用组合式函数库
  • 📚 Vue 源码:深入理解响应式原理
相关推荐
小李子呢02114 小时前
前端八股5---组件通信
前端·javascript·vue.js
weixin_156241575764 小时前
基于django外语学习系统
学习
vmiao4 小时前
【JS进阶】模拟正确处理并渲染后台数据
前端·javascript
Wect4 小时前
JS手撕:函数进阶 & 设计模式解析
前端·javascript·面试
小小的代码里面挖呀挖呀挖4 小时前
恒玄BES蓝牙耳机开发--IIC接口应用
笔记·单片机·物联网·学习·iot
kyriewen114 小时前
每日知识点:this 指向之谜——是谁在 call 我?
前端·javascript·vue.js·前端框架·ecmascript·jquery·html5
浩星4 小时前
electron系列6之性能优化:从启动慢到内存泄漏
前端·javascript·electron
前端那点事4 小时前
Vue3 代码编写规范 | 避坑指南+团队协作标准
vue.js
Ruihong4 小时前
Vue 迁移 React 实战:VuReact 一键自动化转换方案
前端·vue.js