V
第 1.5 章 Vue 3 完整教程
Vue 3 · Composition API · Pinia · Vue Router 4
Vue 是渐进式 JavaScript 框架:易学易用、响应式驱动、中国社区强大,文档中文质量极高。React 教你"用 JS 写 UI",Vue 教你"用增强版 HTML 写应用"。后端转前端想快速出活,Vue 3 + <script setup> 是最友好的一条路。
比Vue 2 vs Vue 3
Vue 3 四大升级:① Composition API(组合式)——按逻辑组织代码,告别 Options API 散在 data/methods/computed 里;② Proxy 响应式——替换 Vue 2 的 Object.defineProperty,性能更好、能检测数组索引和新增属性;③ 更小更快、对 TypeScript 支持更好;④ 新增 Fragments(多根节点)、Teleport(传送门)、Suspense(异步组件)。
Vue 3 推荐用 Composition API + <script setup>语法糖。Options API(选项式)仍兼容,老项目能跑。
模板语法:{{ }}、指令、事件、表单
插值 / 属性 / class / 条件 / 列表 / 事件 / 表单
<template>
<p>{{ msg }}</p> <!-- 文本插值 -->
<p v-html="rawHtml"></p> <!-- 原始 HTML(注意 XSS!) -->
<a :href="url" :class="{ active: isOn }"></a> <!-- : 是 v-bind 缩写 -->
<button v-if="role==='admin'">管理</button> <!-- v-if 不渲染;v-show 只是 display:none -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
<button @click="count++">@ 是 v-on 缩写</button>
<form @submit.prevent="onSubmit"> <!-- .prevent 阻止默认行为 -->
<input v-model="name" /> <!-- 双向绑定 -->
<input v-model.number="age" /> <!-- .number 转数字 -->
<input v-model.lazy="bio" /> <!-- .lazy 改在 change 时同步 -->
</form>
</template>
v-if vs v-show:v-if 真正销毁/重建 DOM,切换开销大但初始不渲染;v-show 只是 CSS 显示切换,频繁切换用 v-show。事件修饰符 .stop/.prevent/.capture/.self/.once,按键修饰符 .enter/.delete,系统修饰符 .ctrl/.alt/.shift/.meta。
响应式核心:ref 与 reactive
<script setup> 中的响应式
<script setup>
import { ref, reactive, computed, watch, watchEffect } from "vue";
const count = ref(0); // 基本类型用 ref,模板里直接 count.value? 不用!模板自动解包
const user = reactive({ name: "小明", age: 18 }); // 对象用 reactive
const double = computed(() => count.value * 2); // 计算属性,有缓存
watch(count, (n, o) => console.log(`${o} -> ${n}`)); // 侦听特定源
watchEffect(() => console.log(count.value)); // 自动追踪用到的响应式源,立即执行一次
function inc() { count.value++; } // JS 里必须 .value
</script>
reactive 解构丢失响应性
const { name, age } = user 会丢掉响应式!要保持响应式用 toRefs(user):const { name, age } = toRefs(user)。ref 在 JS 里必须 .value,模板里自动解包别加 .value——这是新手最常错的两点。
生命周期
| <script setup> 钩子 | 什么时候触发 |
onMounted | 组件挂载到 DOM 后(发请求、操作 DOM)。 |
onUpdated | 响应式状态变更、DOM 更新后。 |
onUnmounted | 组件卸载时(清定时器、解绑事件)。 |
onBeforeMount/Update/Unmount | 对应阶段"之前"。 |
onActivated/onDeactivated | <keep-alive> 缓存组件激活/停用。 |
onErrorCaptured | 捕获子孙组件错误。 |
组件:Props / Emits / 插槽 / 依赖注入
父传子 props,子传父 emits
<!-- Child.vue -->
<script setup>
const props = defineProps({
title: { type: String, required: true },
size: { type: Number, default: 16 },
});
const emit = defineEmits(["update", "delete"]);
</script>
<template>
<h3 @click="emit('update', { title })">{{ title }}</h3>
<!-- 插槽:父组件往这里填内容 -->
<slot />
<slot name="footer" /> <!-- 具名插槽,父用 #footer -->
</template>
<!-- 父组件 -->
<Child :title="t" @update="onUpdate">
<p>默认插槽内容</p>
<template #footer>页脚</template> <!-- # 是 v-slot: 的缩写 -->
</Child>
provide/inject:祖先组件 provide('key', value),后代任意层级 inject('key'),跨层级传值不用 props drilling。组件通信全家桶:props 下传、emits 上抛、provide/inject 跨层、pinia 全局、插槽反向传内容。
组合式函数 Composables:Vue 的逻辑复用
Composable 就是一个 use 开头的函数,把响应式逻辑抽出来复用(等价 React Hooks)。比 Mixin 强:命名不冲突、来源清晰、类型友好。
useMouse / useLocalStorage 两个 Composable
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from "vue";
export function useMouse() {
const x = ref(0), y = ref(0);
const move = (e: MouseEvent) => { x.value = e.clientX; y.value = e.clientY; };
onMounted(() => window.addEventListener("mousemove", move));
onUnmounted(() => window.removeEventListener("mousemove", move));
return { x, y };
}
// 任何组件里:const { x, y } = useMouse(); 拿来即用
// composables/useLocalStorage.ts —— 自动持久化
export function useLocalStorage(key: string, defaultValue: string) {
const data = ref(localStorage.getItem(key) ?? defaultValue);
watch(data, (v) => localStorage.setItem(key, v));
return data;
}
Vue Router 4 + Pinia
路由表 + 导航守卫
// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/user/:id", component: () => import("@/views/User.vue") }, // 懒加载
{ path: "/admin", component: Admin, meta: { requiresAuth: true } },
],
});
// 全局守卫:未登录拦去登录页
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isLoggedIn()) return { path: "/login" };
});
Pinia(Vue 官方推荐状态管理,替代 Vuex)
// stores/user.ts
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({ name: "", token: "" }),
getters: { isLogin: (s) => !!s.token }, // 计算属性
actions: {
async login(account: string, pwd: string) {
/* 调接口,赋值 this.name / this.token */
},
logout() { this.token = ""; this.name = ""; },
},
});
// 组件里:const u = useUserStore(); u.login(a,p); 模板用 u.isLogin
// 解构保持响应式:import { storeToRefs } from 'pinia'; const { name, isLogin } = storeToRefs(u);
Pinia 对比 Vuex:更简单、TypeScript 友好、没有 mutations(直接在 actions 里改 state)、天然模块化。持久化用 pinia-plugin-persistedstate。
完整案例:Vue 3 任务管理应用
技术栈:Vue 3 + TypeScript + Vite + Vue Router 4 + Pinia + Element Plus。目录:src/views/(列表页/详情页)、src/stores/todo.ts(Pinia 存任务列表)、src/composables/useFilter.ts(过滤逻辑)。路由 / 任务列表、/todo/:id 详情;Pinia store 的 state 存 todos: Todo[],actions 写 add/toggle/remove,getters 写 doneCount;Element Plus 的 <el-input><el-button><el-checkbox><el-dialog> 直接搭界面。这套下来就是一个能交付的小工具。
| Vue 3 | React 19 |
| 响应式 | Proxy 自动追踪,ref/reactive 声明即用 | 手动 useState,不可变更新 |
| 模板 | HTML + 指令(v-if/v-for/v-model) | JSX,一切都是 JS |
| 状态管理 | Pinia(官方) | Zustand/Redux Toolkit(生态选择) |
| 学习曲线 | 平缓,中文社区强 | 陡,概念多但天花板高 |
| 生态 | 企业中后台(Element Plus/Ant Design Vue) | 全球最大,跨端(React Native) |
| SSR | Nuxt | Next.js |
本章面试题
Q1. Vue 3 的 Proxy 响应式相比 Vue 2 的 Object.defineProperty 解决了什么问题?
看答案
Vue 2 无法检测对象属性新增/删除和数组下标赋值,要 Vue.set 补救;Proxy 能拦截整个对象,新增属性、数组操作都自动追踪,且性能更好。
Q2. ref 和 reactive 怎么选?解构 reactive 为什么丢响应性?
看答案
基本类型用 ref,对象用 reactive。解构 reactive 拿到的是普通变量副本,断了 Proxy 追踪,要用 toRefs 包装。
Q3. v-if 和 v-show 的区别?v-for 为什么不能和 v-if 同用?
看答案
v-if 销毁/重建 DOM,v-show 切 display。v-for 优先级高于 v-if,同用时每次循环都先判断,性能差且逻辑混乱,应改用 computed 先过滤再 v-for。
Q4. Pinia 相比 Vuex 砍掉了什么?
看答案
没有 mutations,state 直接在 actions 里改;模块化天然(每个 defineStore 独立);TS 推断更好。