文章目录
- 引言
- [Vue 3 学习笔记](#Vue 3 学习笔记)
- 第一部分:基础入门
-
- [1. Vue 是什么](#1. Vue 是什么)
-
- [1.1 关键词拆解](#1.1 关键词拆解)
- [1.2 Vue 的生态圈](#1.2 Vue 的生态圈)
- [1.3 Vue 的核心思想:数据驱动视图](#1.3 Vue 的核心思想:数据驱动视图)
- [2. 环境准备](#2. 环境准备)
-
- [2.1 学习阶段:CDN 引入](#2.1 学习阶段:CDN 引入)
- [2.2 常用 CDN](#2.2 常用 CDN)
- [2.3 下载到本地](#2.3 下载到本地)
- [2.4 版本区别](#2.4 版本区别)
- [2.5 开发工具](#2.5 开发工具)
- [3. 第一个 Vue 程序](#3. 第一个 Vue 程序)
-
- [3.1 三步骤](#3.1 三步骤)
- [3.2 常见错误](#3.2 常见错误)
- [4. Vue 的两种 API 风格](#4. Vue 的两种 API 风格)
-
- [4.1 选项式 API(Options API)](#4.1 选项式 API(Options API))
- [4.2 组合式 API(Composition API)](#4.2 组合式 API(Composition API))
- [4.3 对比](#4.3 对比)
- [5. 插值表达式](#5. 插值表达式)
-
- [5.1 可以写](#5.1 可以写)
- [5.2 不能写](#5.2 不能写)
- [5.3 ⚠️ 不能用在属性里](#5.3 ⚠️ 不能用在属性里)
- 第二部分:核心指令
-
- [6. 指令总览](#6. 指令总览)
- [7. v-for 列表渲染](#7. v-for 列表渲染)
-
- [7.1 基本语法](#7.1 基本语法)
- [7.2 参数](#7.2 参数)
- [7.3 遍历对象](#7.3 遍历对象)
- [7.4 ⚠️ key 的重要性](#7.4 ⚠️ key 的重要性)
- [7.5 实战](#7.5 实战)
- [8. v-bind 属性绑定](#8. v-bind 属性绑定)
-
- [8.1 基本语法](#8.1 基本语法)
- [8.2 常见用法](#8.2 常见用法)
- [8.3 ⚠️ 注意](#8.3 ⚠️ 注意)
- [9. v-if / v-else-if / v-else](#9. v-if / v-else-if / v-else)
-
- [9.1 基本语法](#9.1 基本语法)
- [9.2 实战](#9.2 实战)
- [9.3 原理](#9.3 原理)
- [9.4 ⚠️ 规则](#9.4 ⚠️ 规则)
- [10. v-show 显示隐藏](#10. v-show 显示隐藏)
-
- [10.1 基本语法](#10.1 基本语法)
- [10.2 原理](#10.2 原理)
- [10.3 适用场景](#10.3 适用场景)
- [11. v-model 双向绑定](#11. v-model 双向绑定)
-
- [11.1 基本语法](#11.1 基本语法)
- [11.2 作用](#11.2 作用)
- [11.3 实战](#11.3 实战)
- [11.4 修饰符](#11.4 修饰符)
- [11.5 ⚠️ 注意](#11.5 ⚠️ 注意)
- [12. v-on 事件绑定](#12. v-on 事件绑定)
-
- [12.1 基本语法](#12.1 基本语法)
- [12.2 实战](#12.2 实战)
- [12.3 传参](#12.3 传参)
- [12.4 常见事件](#12.4 常见事件)
- [12.5 修饰符](#12.5 修饰符)
- [12.6 实战:阻止冒泡](#12.6 实战:阻止冒泡)
- [13. v-if 与 v-show 深度对比](#13. v-if 与 v-show 深度对比)
- 第三部分:响应式核心
-
- [14. 什么是响应式](#14. 什么是响应式)
- [15. ref 与 reactive](#15. ref 与 reactive)
-
- [15.1 ref:基本类型](#15.1 ref:基本类型)
- [15.2 reactive:对象类型](#15.2 reactive:对象类型)
- [15.3 对比](#15.3 对比)
- [15.4 推荐用法](#15.4 推荐用法)
- [15.5 解构保持响应式](#15.5 解构保持响应式)
- [16. computed 计算属性](#16. computed 计算属性)
-
- [16.1 作用](#16.1 作用)
- [16.2 基本用法](#16.2 基本用法)
- [16.3 getter / setter](#16.3 getter / setter)
- [16.4 实战:过滤列表](#16.4 实战:过滤列表)
- [16.5 ⚠️ 注意](#16.5 ⚠️ 注意)
- [17. watch 侦听器](#17. watch 侦听器)
-
- [17.1 作用](#17.1 作用)
- [17.2 基本用法](#17.2 基本用法)
- [17.3 监听多个](#17.3 监听多个)
- [17.4 监听对象](#17.4 监听对象)
- [17.5 立即执行](#17.5 立即执行)
- [17.6 实战:搜索防抖](#17.6 实战:搜索防抖)
- [18. computed vs watch vs methods](#18. computed vs watch vs methods)
- 第四部分:生命周期
-
- [19. 生命周期钩子](#19. 生命周期钩子)
- [20. 组合式 API 的生命周期](#20. 组合式 API 的生命周期)
-
- [20.1 对应关系](#20.1 对应关系)
- [20.2 实战](#20.2 实战)
- 第五部分:组件化开发
-
- [21. 组件是什么](#21. 组件是什么)
- [22. 注册组件](#22. 注册组件)
-
- [22.1 全局注册](#22.1 全局注册)
- [22.2 局部注册](#22.2 局部注册)
- [22.3 `<script setup>`(推荐)](#22.3
<script setup>(推荐))
- [23. props 父传子](#23. props 父传子)
-
- [23.1 子组件](#23.1 子组件)
- [23.2 父组件](#23.2 父组件)
- [23.3 ⚠️ 单向数据流](#23.3 ⚠️ 单向数据流)
- [24. emit 子传父](#24. emit 子传父)
-
- [24.1 子组件](#24.1 子组件)
- [24.2 父组件](#24.2 父组件)
- [25. slot 插槽](#25. slot 插槽)
-
- [25.1 默认插槽](#25.1 默认插槽)
- [25.2 具名插槽](#25.2 具名插槽)
- [25.3 作用域插槽](#25.3 作用域插槽)
- [26. script setup 语法糖](#26. script setup 语法糖)
-
- [26.1 基本结构](#26.1 基本结构)
- [26.2 特点](#26.2 特点)
- [26.3 对比](#26.3 对比)
- 第六部分:工程化
-
- [27. Vite 项目搭建](#27. Vite 项目搭建)
-
- [27.1 创建项目](#27.1 创建项目)
- [27.2 目录结构](#27.2 目录结构)
- [28. 项目结构](#28. 项目结构)
- [29. 单文件组件 .vue](#29. 单文件组件 .vue)
-
- [29.1 三个块](#29.1 三个块)
- 第七部分:生态
-
- [30. Vue Router 路由](#30. Vue Router 路由)
-
- [30.1 安装](#30.1 安装)
- [30.2 定义路由](#30.2 定义路由)
- [30.3 挂载](#30.3 挂载)
- [30.4 使用](#30.4 使用)
- [30.5 传参](#30.5 传参)
- [30.6 编程式导航](#30.6 编程式导航)
- [31. Pinia 状态管理](#31. Pinia 状态管理)
-
- [31.1 安装](#31.1 安装)
- [31.2 定义 store](#31.2 定义 store)
- [31.3 使用](#31.3 使用)
- [31.4 挂载](#31.4 挂载)
- [31.5 对比 Vuex](#31.5 对比 Vuex)
- [32. Axios 网络请求](#32. Axios 网络请求)
-
- [32.1 安装](#32.1 安装)
- [32.2 基本用法](#32.2 基本用法)
- [32.3 封装](#32.3 封装)
- [32.4 实战](#32.4 实战)
- 第八部分:实战与调试
-
- [33. Tlias 员工管理实战](#33. Tlias 员工管理实战)
-
- [33.1 需求](#33.1 需求)
- [33.2 核心数据结构](#33.2 核心数据结构)
- [33.3 渲染](#33.3 渲染)
- [33.4 搜索](#33.4 搜索)
- [33.5 多选](#33.5 多选)
- [34. Vue DevTools 调试](#34. Vue DevTools 调试)
-
- [34.1 安装](#34.1 安装)
- [34.2 关键设置](#34.2 关键设置)
- [34.3 验证](#34.3 验证)
- [34.4 图标灰色排查](#34.4 图标灰色排查)
- [34.5 面板功能](#34.5 面板功能)
- 第九部分:避坑与速查
-
- [35. 常见错误汇总](#35. 常见错误汇总)
-
- [35.1 `Vue is not defined`](#35.1
Vue is not defined) - [35.2 页面显示 `{{ message }}`](#35.2 页面显示
{{ message }}) - [35.3 `Cannot read properties of null`](#35.3
Cannot read properties of null) - [35.4 `v-model` 绑定 undefined](#35.4
v-model绑定 undefined) - [35.5 插值表达式用在属性里](#35.5 插值表达式用在属性里)
- [35.6 v-for 忘写 :key](#35.6 v-for 忘写 :key)
- [35.7 修改 props 报错](#35.7 修改 props 报错)
- [35.8 reactive 解构丢失响应式](#35.8 reactive 解构丢失响应式)
- [35.9 ref 在 JS 里忘了 .value](#35.9 ref 在 JS 里忘了 .value)
- [35.10 computed 里写异步](#35.10 computed 里写异步)
- [35.1 `Vue is not defined`](#35.1
- [36. 指令速查表](#36. 指令速查表)
-
- 修饰符
- [组合式 API 速查](#组合式 API 速查)
- [37. 学习路线](#37. 学习路线)
- [附录:Vue 2 vs Vue 3](#附录:Vue 2 vs Vue 3)
引言
本文是一份面向零基础初学者的 Vue 3 学习笔记。阅读本文前,你只需了解 HTML、>CSS 和 JavaScript 的基础语法即可,无需任何前端框架经验。
全文按照「基础 → 指令 → 响应式 → 组件 → 工程化 → 生态 → 实战」的顺序循序渐进:>先认识 Vue 是什么、如何搭建环境,再掌握核心指令与响应式原理,随后进入组件化开发与工程化实践,最后通过 Vue Router、Pinia、Axios 等生态工具完成一个完整的员工管理实战项目。
学完本文,你将能够独立使用 Vue 3 + Vite 开发中小型前端项目,并具备排查常见错误、使用 DevTools 调试的能力。建议学习周期为 2-3 周:第一周打好基础与指令,第二周攻克组件与工程化,第三周完成实战与生态整合。
Vue 3 学习笔记
目录
第一部分:基础入门
第二部分:核心指令
- 指令总览
- [v-for 列表渲染](#v-for 列表渲染)
- [v-bind 属性绑定](#v-bind 属性绑定)
- [v-if / v-else-if / v-else](#v-if / v-else-if / v-else)
- [v-show 显示隐藏](#v-show 显示隐藏)
- [v-model 双向绑定](#v-model 双向绑定)
- [v-on 事件绑定](#v-on 事件绑定)
- [v-if 与 v-show 深度对比](#v-if 与 v-show 深度对比)
第三部分:响应式核心
- 什么是响应式
- [ref 与 reactive](#ref 与 reactive)
- [computed 计算属性](#computed 计算属性)
- [watch 侦听器](#watch 侦听器)
- [computed vs watch vs methods](#computed vs watch vs methods)
第四部分:生命周期
- 生命周期钩子
- [组合式 API 的生命周期](#组合式 API 的生命周期)
第五部分:组件化开发
- 组件是什么
- 注册组件
- [props 父传子](#props 父传子)
- [emit 子传父](#emit 子传父)
- [slot 插槽](#slot 插槽)
- [script setup 语法糖](#script setup 语法糖)
第六部分:工程化
- [Vite 项目搭建](#Vite 项目搭建)
- 项目结构
- [单文件组件 .vue](#单文件组件 .vue)
第七部分:生态
- [Vue Router 路由](#Vue Router 路由)
- [Pinia 状态管理](#Pinia 状态管理)
- [Axios 网络请求](#Axios 网络请求)
第八部分:实战与调试
- [Tlias 员工管理实战](#Tlias 员工管理实战)
- [Vue DevTools 调试](#Vue DevTools 调试)
第九部分:避坑与速查
第一部分:基础入门
1. Vue 是什么
Vue 是一款用于构建用户界面 的渐进式 JavaScript 框架。
1.1 关键词拆解
| 关键词 | 含义 |
|---|---|
| 构建用户界面 | 把数据渲染成页面 |
| 渐进式 | 可以只用一部分,也可以全用 |
| 框架 | 一套有规则的开发方案 |
| JavaScript | 基于 JS,不是新语言 |
1.2 Vue 的生态圈
Vue 核心(声明式渲染 + 组件系统)
↓
VueRouter(客户端路由)
↓
Pinia(状态管理,Vuex 的替代)
↓
Vite / Webpack(构建工具)
1.3 Vue 的核心思想:数据驱动视图
传统写法:
js
document.getElementById('msg').innerText = 'Hello'
Vue 写法:
js
message.value = 'Hello' // 页面自动更新
你只关心数据,DOM 交给 Vue。
2. 环境准备
2.1 学习阶段:CDN 引入
ESM 模块版:
html
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
</script>
全局构建版:
html
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue
</script>
2.2 常用 CDN
| 来源 | 地址 |
|---|---|
| unpkg | https://unpkg.com/vue@3/dist/vue.global.js |
| jsDelivr | https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js |
| npmmirror | https://registry.npmmirror.com/vue/3.4.21/files/dist/vue.global.js |
2.3 下载到本地
- 浏览器打开
https://unpkg.com/vue@3.4.21/dist/vue.global.js - 右键另存为
vue.js - 和 HTML 放同一目录
<script src="./vue.js"></script>
2.4 版本区别
| 文件 | 用途 | 是否含编译器 |
|---|---|---|
vue.global.js |
全局开发版 | ✅ |
vue.global.prod.js |
全局生产版 | ✅ |
vue.esm-browser.js |
ESM 开发版 | ✅ |
vue.runtime.global.js |
仅运行时 | ❌ |
写 {``{ }} 模板必须用含编译器的版本。
2.5 开发工具
| 工具 | 用途 |
|---|---|
| VS Code | 编辑器 |
| Live Server 插件 | 本地服务器 |
| Vue DevTools | 浏览器调试插件 |
| Vue VSCode Snippets | 代码片段 |
3. 第一个 Vue 程序
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue 入门</title>
</head>
<body>
<div id="app">
<h1>{{ message }}</h1>
<h2>{{ count }}</h2>
</div>
<script src="./vue.js"></script>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello World!',
count: 100
}
}
}).mount('#app')
</script>
</body>
</html>
3.1 三步骤
- 引入 Vue 模块
- 创建应用实例 :
createApp({...}) - 准备元素 :
<div id="app"> - 挂载 :
.mount('#app')
3.2 常见错误
| 错误 | 原因 |
|---|---|
Vue is not defined |
大小写错,应该大写 Vue |
页面显示 {``{ message }} |
没挂载 / Vue 没加载 |
Cannot read properties of null |
#app 元素不存在 |
4. Vue 的两种 API 风格
4.1 选项式 API(Options API)
js
createApp({
data() {
return { message: 'Hello', count: 0 }
},
methods: {
add() { this.count++ }
},
computed: {
double() { return this.count * 2 }
}
}).mount('#app')
4.2 组合式 API(Composition API)
js
const { createApp, ref, computed } = Vue
createApp({
setup() {
const message = ref('Hello')
const count = ref(0)
function add() { count.value++ }
const double = computed(() => count.value * 2)
return { message, count, add, double }
}
}).mount('#app')
4.3 对比
| 对比项 | 选项式 | 组合式 |
|---|---|---|
| 写法 | data/methods/computed |
setup() + ref |
this |
用 this.xxx |
不用 this |
| 适合 | 简单页面 | 复杂项目 |
| Vue 3 推荐 | --- | ✅ |
大项目用组合式,逻辑可以按功能分组,而不是按选项分散。
5. 插值表达式
html
<div id="app">
<h1>{{ message }}</h1>
<p>{{ count + 1 }}</p>
<p>{{ message.toUpperCase() }}</p>
<p>{{ count > 50 ? '大' : '小' }}</p>
</div>
5.1 可以写
- 变量
- 算术运算
a + b - 三元
a ? b : c - 字符串方法
msg.toUpperCase() - 数组/对象访问
arr[0]、obj.name
5.2 不能写
if/for语句- 赋值
a = 1 - 声明
let x = 1
5.3 ⚠️ 不能用在属性里
html
<!-- 错误 -->
<img src="{{ image }}">
<!-- 正确 -->
<img :src="image">
第二部分:核心指令
6. 指令总览
指令:HTML 标签上带
v-前缀的特殊属性。
| 指令 | 缩写 | 作用 |
|---|---|---|
v-for |
--- | 列表渲染 |
v-bind |
: |
属性绑定 |
v-if |
--- | 条件渲染 |
v-else-if |
--- | 条件链 |
v-else |
--- | 条件兜底 |
v-show |
--- | 显示/隐藏 |
v-model |
--- | 双向绑定 |
v-on |
@ |
事件绑定 |
7. v-for 列表渲染
7.1 基本语法
html
<tr v-for="(item, index) in items" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
</tr>
7.2 参数
| 参数 | 含义 |
|---|---|
items |
数组 |
item |
当前元素 |
index |
索引(可省略) |
:key |
唯一标识 |
7.3 遍历对象
html
<div v-for="(value, key, index) in obj" :key="key">
{{ index }} - {{ key }} - {{ value }}
</div>
7.4 ⚠️ key 的重要性
推荐: :key="item.id"(唯一 id)
不推荐: :key="index"(会变)
原因: 用 index 做 key,删除中间项时,后面所有项的 key 都变,Vue 会重新渲染,性能差且可能出错。
7.5 实战
html
<tbody>
<tr v-for="(e, index) in empList" :key="e.id">
<td>{{ index + 1 }}</td>
<td>{{ e.name }}</td>
<td>{{ e.gender === 1 ? '男' : '女' }}</td>
<td><img :src="e.image" class="avatar"></td>
<td>{{ e.job }}</td>
<td>{{ e.entrydate }}</td>
<td>{{ e.updatetime }}</td>
</tr>
</tbody>
8. v-bind 属性绑定
8.1 基本语法
html
<!-- 完整 -->
<img v-bind:src="image">
<!-- 缩写 -->
<img :src="image">
8.2 常见用法
html
<!-- 绑定 src -->
<img :src="emp.image">
<!-- 绑定 href -->
<a :href="url">链接</a>
<!-- 绑定 class(对象语法) -->
<div :class="{ active: isActive, 'text-red': isRed }"></div>
<!-- 绑定 class(数组语法) -->
<div :class="['btn', type, isActive ? 'active' : '']"></div>
<!-- 绑定 style -->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<!-- 绑定 disabled -->
<button :disabled="isDisabled">按钮</button>
<!-- 绑定多个属性 -->
<img v-bind="{ src: imgUrl, alt: imgAlt }">
8.3 ⚠️ 注意
动态为标签属性绑定值,不能用插值表达式,必须用
v-bind。且绑定的数据必须在
data中定义。
html
<!-- 错误 -->
<img src="{{ image }}">
<!-- 正确 -->
<img :src="image">
9. v-if / v-else-if / v-else
9.1 基本语法
html
<span v-if="gender === 1">男生</span>
<span v-else-if="gender === 2">女生</span>
<span v-else>未知</span>
9.2 实战
html
<td>
<span v-if="e.job === 1">班主任</span>
<span v-else-if="e.job === 2">讲师</span>
<span v-else-if="e.job === 3">学工主管</span>
<span v-else-if="e.job === 4">教研主管</span>
<span v-else-if="e.job === 5">咨询师</span>
<span v-else>其他</span>
</td>
9.3 原理
条件不成立 → 元素不渲染(DOM 里没有)。
9.4 ⚠️ 规则
v-else-if必须在v-if之后v-else必须在最后- 三者必须紧挨着,中间不能插入其他元素
10. v-show 显示隐藏
10.1 基本语法
html
<span v-show="e.job === 1">班主任</span>
<span v-show="e.job === 2">讲师</span>
10.2 原理
通过 CSS display: none 控制。
F12 里看到:
html
<td>
<span>班主任</span>
<span style="display: none;">讲师</span>
</td>
10.3 适用场景
频繁切换显示/隐藏。
11. v-model 双向绑定
11.1 基本语法
html
<input type="text" v-model="searchForm.name">
11.2 作用
- 数据变 → 输入框变
- 输入框变 → 数据变
11.3 实战
html
<form class="search-form">
<label>姓名:</label>
<input type="text" v-model="searchForm.name" placeholder="请输入姓名">
<label>性别:</label>
<select v-model="searchForm.gender">
<option value=""></option>
<option value="1">男</option>
<option value="2">女</option>
</select>
<button type="button" @click="search">查询</button>
<button type="button" @click="clear">清空</button>
</form>
js
const { createApp } = Vue
createApp({
data() {
return {
searchForm: {
name: '',
gender: '',
job: ''
}
}
},
methods: {
search() {
console.log(this.searchForm)
}
}
}).mount('#app')
11.4 修饰符
| 修饰符 | 作用 | 例子 |
|---|---|---|
.trim |
去首尾空格 | v-model.trim="name" |
.number |
转数字 | v-model.number="age" |
.lazy |
失焦才同步 | v-model.lazy="msg" |
11.5 ⚠️ 注意
v-model 绑定的变量必须在 data 中定义。
12. v-on 事件绑定
12.1 基本语法
html
<!-- 完整 -->
<button v-on:click="handle">点我</button>
<!-- 缩写 -->
<button @click="handle">点我</button>
12.2 实战
html
<button @click="handle">点我</button>
js
const { createApp } = Vue
createApp({
data() {
return {}
},
methods: {
handle() {
console.log('试试就试试')
}
}
}).mount('#app')
12.3 传参
html
<button @click="add(1)">加1</button>
<button @click="del(emp)">删除</button>
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<div id="app">
<p>{{ count }}</p>
<button @click="add(1)">加1</button>
</div>
<script src="./vue.js"></script>
<script>
const { createApp } = Vue
createApp({
data() {
return {
count: 0
}
},
methods: {
add(n) {
this.count += n
},
del(emp) {
console.log('删除', emp)
}
}
}).mount('#app')
</script>
</body>
</html>
12.4 常见事件
| 事件 | 触发 |
|---|---|
@click |
点击 |
@dblclick |
双击 |
@input |
输入 |
@change |
值改变 |
@submit |
表单提交 |
@keyup.enter |
回车 |
@keyup.esc |
ESC |
@mouseover |
鼠标移入 |
@mouseleave |
鼠标移出 |
12.5 修饰符
| 修饰符 | 作用 | 例子 |
|---|---|---|
.stop |
阻止冒泡 | @click.stop |
.prevent |
阻止默认 | @submit.prevent |
.self |
只点自己 | @click.self |
.once |
只一次 | @click.once |
.enter |
回车 | @keyup.enter |
12.6 实战:阻止冒泡
html
<tr @click="selectRow(emp)">
<td>
<button @click.stop="edit(emp)">编辑</button>
<button @click.stop="remove(emp)">删除</button>
</td>
</tr>
13. v-if 与 v-show 深度对比
| 对比项 | v-if | v-show |
|---|---|---|
| 原理 | 不渲染 / 销毁重建 | display: none |
| DOM | 有 / 没有 | 一直有 |
| 初始开销 | 高 | 低 |
| 切换开销 | 高 | 低 |
| 场景 | 不频繁切换 | 频繁切换 |
支持 v-else |
✅ | ❌ |
口诀:频繁切换用 v-show,条件稳定用 v-if。
第三部分:响应式核心
14. 什么是响应式
数据变 → 页面自动变。
Vue 3 用 Proxy 实现响应式:
js
const obj = reactive({ count: 0 })
obj.count++ // 页面自动更新
对比 Vue 2: Vue 2 用 Object.defineProperty,无法检测新增属性、数组索引赋值。Vue 3 的 Proxy 解决了这些问题。
15. ref 与 reactive
15.1 ref:基本类型
js
const count = ref(0)
const msg = ref('Hello')
console.log(count.value) // 0
count.value++ // 修改
模板里不用 .value:
html
<p>{{ count }}</p>
JS 里必须 .value:
js
count.value = 10
15.2 reactive:对象类型
js
const state = reactive({
name: '张三',
age: 18
})
state.name = '李四' // 直接改,不用 .value
15.3 对比
| 对比项 | ref | reactive |
|---|---|---|
| 适用 | 基本类型、任意 | 对象、数组 |
| JS 访问 | .value |
直接访问 |
| 模板访问 | 直接 | 直接 |
| 替换整个对象 | ✅ | ❌ |
| 解构 | 需 toRefs |
需 toRefs |
15.4 推荐用法
- 基本类型 →
ref - 对象/数组 →
reactive - 不确定 → 全用
ref(模板里不用.value,更省心)
15.5 解构保持响应式
js
const state = reactive({ name: '张三', age: 18 })
const { name, age } = toRefs(state) // 保持响应式
16. computed 计算属性
16.1 作用
依赖其他数据,自动计算,有缓存。
16.2 基本用法
js
const count = ref(0)
const double = computed(() => count.value * 2)
html
<p>{{ count }}</p>
<p>{{ double }}</p> <!-- count 变,double 自动变 -->
16.3 getter / setter
js
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get: () => firstName.value + lastName.value,
set: (val) => {
firstName.value = val[0]
lastName.value = val.slice(1)
}
})
16.4 实战:过滤列表
js
const filteredEmployees = computed(() => {
return employees.value.filter(emp => {
const matchName = !activeName.value || emp.name.includes(activeName.value)
const matchGender = !activeGender.value || emp.gender === activeGender.value
return matchName && matchGender
})
})
16.5 ⚠️ 注意
- computed 里不要写异步 、不要改其他数据
- 依赖不变时,computed 不会重新计算(有缓存)
17. watch 侦听器
17.1 作用
监听数据变化,执行副作用(如请求、打印、改其他数据)。
17.2 基本用法
js
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变成 ${newVal}`)
})
17.3 监听多个
js
watch([count, msg], ([newCount, newMsg], [oldCount, oldMsg]) => {
console.log('变了')
})
17.4 监听对象
js
const state = reactive({ name: '张三', age: 18 })
// 监听整个对象(深度)
watch(state, (newVal) => {
console.log('变了')
}, { deep: true })
// 只监听某个属性
watch(() => state.name, (newVal) => {
console.log('name 变了')
})
17.5 立即执行
js
watch(count, (val) => {
console.log(val)
}, { immediate: true })
17.6 实战:搜索防抖
js
watch(searchName, (val) => {
// 输入停止 500ms 后才查询
clearTimeout(timer)
timer = setTimeout(() => {
doSearch(val)
}, 500)
})
18. computed vs watch vs methods
| 对比项 | computed | watch | methods |
|---|---|---|---|
| 用途 | 派生新数据 | 监听变化做副作用 | 事件处理 |
| 缓存 | ✅ | --- | ❌ |
| 异步 | ❌ | ✅ | ✅ |
| 返回值 | 必须 | 无 | 可有可无 |
| 依赖自动追踪 | ✅ | 需手动 | --- |
选择建议:
- 要派生数据 →
computed - 要监听变化做副作用 (请求、日志) →
watch - 要响应用户事件 →
methods
第四部分:生命周期
19. 生命周期钩子
Vue 组件从创建到销毁的过程:
beforeCreate → created → beforeMount → mounted
→ beforeUpdate → updated
→ beforeUnmount → unmounted
| 钩子 | 时机 | 常用场景 |
|---|---|---|
beforeCreate |
实例创建前 | 很少用 |
created |
实例创建后 | 请求数据(无 DOM) |
beforeMount |
挂载前 | 很少用 |
mounted |
挂载后 | 操作 DOM、请求数据 |
beforeUpdate |
更新前 | 很少用 |
updated |
更新后 | 操作更新后的 DOM |
beforeUnmount |
卸载前 | 清理定时器、事件监听 |
unmounted |
卸载后 | 很少用 |
20. 组合式 API 的生命周期
js
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件挂载了')
// 请求数据、操作 DOM
})
onUnmounted(() => {
console.log('组件卸载了')
// 清理定时器
})
20.1 对应关系
| 选项式 | 组合式 |
|---|---|
beforeCreate |
setup() |
created |
setup() |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
20.2 实战
js
onMounted(() => {
// 页面加载后请求员工列表
fetchEmployees()
})
onUnmounted(() => {
clearInterval(timer)
})
第五部分:组件化开发
21. 组件是什么
组件 = 可复用的 UI 片段 + 逻辑。
比如一个按钮、一个表格、一个弹窗,都可以做成组件。
22. 注册组件
22.1 全局注册
js
app.component('MyButton', {
template: '<button>点我</button>'
})
22.2 局部注册
js
import MyButton from './MyButton.vue'
export default {
components: { MyButton }
}
22.3 <script setup>(推荐)
vue
<script setup>
import MyButton from './MyButton.vue'
</script>
<template>
<MyButton />
</template>
导入即注册,不用写 components。
23. props 父传子
23.1 子组件
vue
<script setup>
const props = defineProps({
title: String,
count: {
type: Number,
default: 0,
required: false
}
})
</script>
<template>
<h3>{{ title }} - {{ count }}</h3>
</template>
23.2 父组件
vue
<template>
<Child title="你好" :count="10" />
</template>
23.3 ⚠️ 单向数据流
props 只能读,不能改。 想改要 emit 通知父组件。
24. emit 子传父
24.1 子组件
vue
<script setup>
const emit = defineEmits(['add', 'delete'])
function handleAdd() {
emit('add', 1)
}
function handleDelete(id) {
emit('delete', id)
}
</script>
<template>
<button @click="handleAdd">加1</button>
<button @click="handleDelete(5)">删除</button>
</template>
24.2 父组件
vue
<template>
<Child @add="onAdd" @delete="onDelete" />
</template>
<script setup>
function onAdd(n) { console.log('加了', n) }
function onDelete(id) { console.log('删了', id) }
</script>
25. slot 插槽
25.1 默认插槽
vue
<!-- 子组件 -->
<template>
<div class="card">
<slot>默认内容</slot>
</div>
</template>
<!-- 父组件 -->
<Card>
<p>我是插槽内容</p>
</Card>
25.2 具名插槽
vue
<!-- 子组件 -->
<slot name="header"></slot>
<slot name="footer"></slot>
<!-- 父组件 -->
<template #header>
<h1>标题</h1>
</template>
<template #footer>
<p>底部</p>
</template>
25.3 作用域插槽
vue
<!-- 子组件 -->
<slot :user="user"></slot>
<!-- 父组件 -->
<template #default="{ user }">
{{ user.name }}
</template>
26. script setup 语法糖
<script setup> 是 Vue 3.2+ 的推荐写法。
26.1 基本结构
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function add() { count.value++ }
</script>
<template>
<button @click="add">{{ count }}</button>
<p>{{ double }}</p>
</template>
26.2 特点
- 顶层变量、函数自动暴露给模板
- 不需要
return - 不需要
components注册 - 组件名默认是文件名
26.3 对比
| 对比项 | 普通 setup | <script setup> |
|---|---|---|
| 模板暴露 | 需 return |
自动 |
| 组件注册 | 需 components |
自动 |
| 代码量 | 多 | 少 |
| 推荐 | --- | ✅ |
第六部分:工程化
27. Vite 项目搭建
27.1 创建项目
bash
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
27.2 目录结构
my-vue-app/
├── index.html
├── package.json
├── vite.config.js
├── src/
│ ├── main.js
│ ├── App.vue
│ ├── components/
│ └── assets/
└── public/
28. 项目结构
src/
├── main.js # 入口
├── App.vue # 根组件
├── components/ # 通用组件
├── views/ # 页面
├── router/ # 路由
├── stores/ # Pinia 状态
├── api/ # 接口
├── utils/ # 工具
└── assets/ # 静态资源
29. 单文件组件 .vue
vue
<template>
<div class="box">
<h1>{{ title }}</h1>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('Hello')
</script>
<style scoped>
.box { padding: 20px; }
</style>
29.1 三个块
| 块 | 作用 |
|---|---|
<template> |
模板(必须) |
<script setup> |
逻辑(推荐) |
<style scoped> |
样式(scoped 局部) |
第七部分:生态
30. Vue Router 路由
30.1 安装
bash
npm install vue-router@4
30.2 定义路由
js
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
30.3 挂载
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
30.4 使用
vue
<template>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view />
</template>
30.5 传参
js
// 路由定义
const routes = [
{ path: '/user/:id', component: User }
]
// 跳转
router.push('/user/123')
// 获取
const route = useRoute()
console.log(route.params.id)
30.6 编程式导航
js
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/about')
router.replace('/about')
router.back()
31. Pinia 状态管理
31.1 安装
bash
npm install pinia
31.2 定义 store
js
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})
31.3 使用
vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
<template>
<p>{{ store.count }}</p>
<p>{{ store.double }}</p>
<button @click="store.increment">+1</button>
</template>
31.4 挂载
js
// main.js
import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')
31.5 对比 Vuex
| 对比项 | Vuex | Pinia |
|---|---|---|
| 版本 | Vue 2/3 | Vue 3 |
| 写法 | mutations/actions | 直接改 |
| TS 支持 | 一般 | ✅ 好 |
| 推荐 | 旧项目 | ✅ 新项目 |
32. Axios 网络请求
32.1 安装
bash
npm install axios
32.2 基本用法
js
import axios from 'axios'
const res = await axios.get('/api/employees')
console.log(res.data)
await axios.post('/api/employees', { name: '张三' })
await axios.put('/api/employees/1', { name: '李四' })
await axios.delete('/api/employees/1')
32.3 封装
js
// api/request.js
import axios from 'axios'
const request = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
})
request.interceptors.request.use(config => {
// 加 token
config.headers.Authorization = localStorage.getItem('token')
return config
})
request.interceptors.response.use(
res => res.data,
err => {
console.error(err)
return Promise.reject(err)
}
)
export default request
32.4 实战
js
import request from '@/api/request'
async function fetchEmployees() {
const data = await request.get('/employees')
employees.value = data
}
onMounted(() => {
fetchEmployees()
})
第八部分:实战与调试
33. Tlias 员工管理实战
33.1 需求
- 登录 / 退出
- 搜索:姓名、性别、职位
- 表格:10 条数据
- 新增 / 编辑 / 删除
- 批量删除
- 行选中
33.2 核心数据结构
js
const employees = ref([
{
name: '令狐冲',
gender: '男',
avatar: '🧑',
position: '讲师',
hireDate: '2021-06-15',
lastOpTime: '2024-09-16 15:30'
}
])
33.3 渲染
html
<tr v-for="emp in filteredEmployees" :key="emp.name">
<td>{{ emp.name }}</td>
<td>{{ emp.gender }}</td>
<td>
<span v-if="emp.gender === '女'" class="avatar female">👩</span>
<span v-else-if="emp.gender === '男'" class="avatar male">🧑</span>
<span v-else class="avatar unknown">❓</span>
</td>
<td>
<span v-if="emp.position === '讲师'">🎓 {{ emp.position }}</span>
<span v-else-if="emp.position === '班主任'">📋 {{ emp.position }}</span>
<span v-else>{{ emp.position }}</span>
</td>
<td>{{ emp.hireDate }}</td>
<td>{{ emp.lastOpTime }}</td>
<td>
<button @click.stop="edit(emp)">编辑</button>
<button @click.stop="remove(emp)">删除</button>
</td>
</tr>
33.4 搜索
js
const searchName = ref('')
const activeName = ref('')
function search() {
activeName.value = searchName.value
}
const filteredEmployees = computed(() => {
return employees.value.filter(emp => {
return !activeName.value || emp.name.includes(activeName.value)
})
})
33.5 多选
js
const selectedNames = ref([])
const isAllSelected = computed(() => {
return filteredEmployees.value.length > 0 &&
selectedNames.value.length === filteredEmployees.value.length
})
function toggleSelectAll() {
if (isAllSelected.value) {
selectedNames.value = []
} else {
selectedNames.value = filteredEmployees.value.map(e => e.name)
}
}
34. Vue DevTools 调试
34.1 安装
| 浏览器 | 地址 |
|---|---|
| Chrome | Chrome 应用商店搜 Vue.js devtools |
| Edge | Edge 加载项搜 Vue.js devtools |
| Firefox | Firefox 附加组件搜 Vue.js devtools |
34.2 关键设置
file:// 页面必须开:
chrome://extensions/ → Vue.js devtools → 详情 → 允许访问文件 URL
推荐用 Live Server (http://127.0.0.1:5500/),不受限制。
34.3 验证
- 打开 Vue 页面
- F12 → 顶部有 Vue 标签
- 工具栏 Vue 图标绿色
34.4 图标灰色排查
| 原因 | 解决 |
|---|---|
| 页面不是 Vue | 切到 Vue 页面 |
| 没刷新 | F5 |
| file:// 限制 | 开"允许访问文件 URL" |
| 版本不对 | v7 只支持 Vue 3,Vue 2 用 v6 |
34.5 面板功能
| 面板 | 作用 |
|---|---|
| 组件树 | 查看嵌套 |
| State | 实时查看/改数据 |
| Events | 事件触发 |
| Timeline | 性能 |
第九部分:避坑与速查
35. 常见错误汇总
35.1 Vue is not defined
原因: 大小写错。
js
vue // ❌
Vue // ✅
35.2 页面显示 {``{ message }}
原因:
- Vue 没加载(CDN 挂了 / 路径错)
type="module"+file://被 CORS 拦- 忘写
.mount('#app') #appid 不一致
排查: F12 → Console → Vue.version,有输出说明 Vue 加载成功。
35.3 Cannot read properties of null
原因: #app 元素不存在。
解决: 检查 HTML 里有没有 <div id="app">,且 <script> 在它之后。
35.4 v-model 绑定 undefined
原因: 变量没在 data() 里定义。
解决: 在 return 对象里加上。
35.5 插值表达式用在属性里
html
<!-- 错误 -->
<img src="{{ image }}">
<!-- 正确 -->
<img :src="image">
35.6 v-for 忘写 :key
后果: 警告、性能差、渲染错乱。
解决: :key="item.id",不要用 index。
35.7 修改 props 报错
原因: props 是只读的。
解决: 子组件 emit 事件,父组件改数据。
35.8 reactive 解构丢失响应式
js
const state = reactive({ name: '张三' })
const { name } = state // ❌ 丢失响应式
// 正确
const { name } = toRefs(state) // ✅
35.9 ref 在 JS 里忘了 .value
js
const count = ref(0)
count = 1 // ❌
count.value = 1 // ✅
35.10 computed 里写异步
computed 必须是同步的。 异步用 watch 或 onMounted。
36. 指令速查表
| 指令 | 缩写 | 作用 | 示例 |
|---|---|---|---|
v-for |
--- | 列表 | v-for="item in list" |
v-bind |
: |
属性 | :src="img" |
v-if |
--- | 条件 | v-if="isShow" |
v-else-if |
--- | 条件链 | v-else-if="type===2" |
v-else |
--- | 兜底 | v-else |
v-show |
--- | 显示隐藏 | v-show="isShow" |
v-model |
--- | 双向 | v-model="name" |
v-on |
@ |
事件 | @click="handle" |
修饰符
| 修饰符 | 用于 | 作用 |
|---|---|---|
.stop |
v-on | 阻止冒泡 |
.prevent |
v-on | 阻止默认 |
.self |
v-on | 只点自己 |
.once |
v-on | 只一次 |
.enter |
v-on | 回车 |
.trim |
v-model | 去空格 |
.number |
v-model | 转数字 |
.lazy |
v-model | 失焦同步 |
组合式 API 速查
| API | 作用 |
|---|---|
ref |
基本类型响应式 |
reactive |
对象响应式 |
computed |
计算属性 |
watch |
侦听器 |
onMounted |
挂载后 |
onUnmounted |
卸载后 |
defineProps |
定义 props |
defineEmits |
定义 emits |
37. 学习路线
第一阶段:基础
- HTML / CSS / JS
- Vue 3 快速入门
- 指令:v-for / v-bind / v-if / v-model / v-on
- computed / watch
第二阶段:进阶
- 组件化(props / emit / slot)
- 生命周期
<script setup>- 组件通信(provide/inject)
第三阶段:生态
- Vue Router
- Pinia
- Axios
- Element Plus
第四阶段:工程化
- Vite
- 项目结构
- TypeScript + Vue
- 打包部署
推荐资源
| 资源 | 地址 |
|---|---|
| Vue 3 官方 | https://cn.vuejs.org/ |
| Vue Router | https://router.vuejs.org/zh/ |
| Pinia | https://pinia.vuejs.org/zh/ |
| Vite | https://cn.vitejs.dev/ |
| Element Plus | https://element-plus.org/zh-CN/ |
| VueUse | https://vueuse.org/ |
附录:Vue 2 vs Vue 3
| 对比项 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy |
| API | 选项式 | 选项式 + 组合式 |
| 模板根 | 只能一个 | 可以多个 |
| 生命周期 | beforeDestroy | beforeUnmount |
| v-model | value + input | modelValue + update |
| Teleport | ❌ | ✅ |
| 性能 | 一般 | 更快 |
| TS | 一般 | ✅ |