第一部分:入门基础
1. Vue 3 概述
Vue 是什么?MVVM 模式简介
| 主题 | 说明 |
|---|---|
| Vue 是什么? | Vue(读音 /vjuː/)是一个用于构建用户界面的渐进式 JavaScript 框架。它由尤雨溪开发,核心库只关注视图层,易于上手,且能与现代工具链和第三方库结合,适用于从小型页面到大型单页应用(SPA)的开发。 |
| MVVM 模式简介 | Vue 基于 MVVM(Model-View-ViewModel)架构模式: • Model:应用程序的数据层(如用户信息、API 数据) • View:用户看到的界面(DOM) • ViewModel:Vue 实例,负责连接 Model 和 View,实现数据双向绑定。当 Model 变化时,View 自动更新;当用户操作 View(如输入),Model 也会自动更新。 |
ASCII 图示:MVVM 架构
+--------+ +------------+ +-------+
| Model |<--->| ViewModel |<--->| View |
+--------+ +------------+ +-------+
(数据) (Vue 实例) (界面)
Vue 3 与 Vue 2 的主要区别
| 对比项 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式系统 | 使用 Object.defineProperty() 劫持属性,无法监听数组索引变化或对象属性动态添加 | 使用 Proxy 实现响应式,支持监听数组索引变化和对象属性动态添加 |
| API 风格 | 主要使用 Options API(data, methods, computed 分散) | 支持 Options API 和 Composition API,可按功能组织代码 |
| 组件模板 | 单根元素(一个顶层 div) | 支持多根元素(Fragments) |
| TypeScript 支持 | 支持但类型推断较弱 | 完整的类型定义,IDE 智能提示优秀 |
| 打包体积 | 较大,Tree-shaking 效果一般 | 更小,Tree-shaking 效果更好 |
| 全局 API | 挂在 Vue 对象上(如 Vue.component) | 通过 createApp 创建隔离实例 |
| 生命周期钩子 | beforeDestroy, destroyed | beforeUnmount, unmounted |
| 性能 | 良好 | 更优:静态提升、更高效 diff 算法 |
Vue 3 的核心优势:Composition API、性能提升、TypeScript 支持
| 特性 | 说明 |
|---|---|
| Composition API | 允许按功能组织代码,逻辑复用更方便,适合复杂组件 |
| 性能提升 | • 使用 Proxy 提升响应式效率 • 编译器静态提升(Static Hoisting) • 更高效的 diff 算法 |
| TypeScript 支持 | 完整的类型定义,IDE 智能提示优秀,减少运行时错误 |
官方文档与社区资源
| 资源类型 | 名称 / 链接 |
|---|---|
| 官方文档 | https://vuejs.org |
| 中文文档 | https://cn.vuejs.org |
| GitHub 仓库 | https://github.com/vuejs/core |
| 构建工具 | Vite |
| 开发者工具 | Vue Devtools(浏览器插件) |
| 社区 | Vue Land Discord |
| 生态库 | Pinia(状态管理)、Vue Router(路由) |
最小可用示例
CDN 方式:无需构建工具,直接在浏览器中运行 Vue 3 应用。
HTML 代码:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Vue 3 最小示例</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<p>{{ message }}</p>
<button @click="reverse">反转文字</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
message: 'Hello Vue 3!'
}
},
methods: {
reverse() {
this.message = this.message.split('').reverse().join('');
}
}
}).mount('#app');
</script>
</body>
</html>
运行效果:页面显示 “Hello Vue 3!”,点击按钮后变为 “!3 eueV olleH”
ASCII 图示:Vue 应用启动流程
+---------------------+
| index.html |
| <div id="app"> |
+----------+----------+
|
v
+---------------------+
| createApp({...}) |
| .mount('#app') |
+----------+----------+
|
v
+---------------------+
| 渲染模板 |
| {{ message }} |
| <button @click> |
+---------------------+
2. 环境搭建
构建方式对比
| 方式 | 说明 | 操作步骤与命令 | 最小可用代码示例 |
|---|---|---|---|
| CDN 引入(快速上手) | 无需安装 Node.js 或构建工具,直接在 HTML 中通过 <script> 标签引入 Vue 3,适合学习和原型开发。 | 1. 创建 index.html 2. 引入 Vue 3 CDN 链接 3. 编写 Vue 应用代码 | html<!DOCTYPE html><html><head> <title>Vue 3 CDN 示例</title> <script src="https://unpkg.com/vue@3"></script></head><body> <div id="app"> {{ message }} </div> <script> const { createApp } = Vue createApp({ data() { return { message: 'Hello from CDN!' } } }).mount('#app') </script></body></html> |
| 使用 Vite 创建项目(推荐) | Vite 是 Vue 作者尤雨溪开发的现代前端构建工具,支持极速冷启动和热更新,是 Vue 3 官方推荐的脚手架。 | 1. 打开终端 2. 运行命令: npm create vite@latest my-vue-app -- --template vue3. 进入项目: cd my-vue-app4. 安装依赖: npm install5. 启动开发服务器: npm run dev | src/main.js 示例:jsimport { createApp } from 'vue'import App from './App.vue'createApp(App).mount('#app')src/App.vue 示例:vue<template> <h1>{{ msg }}</h1></template><script>export default { data() { return { msg: 'Hello from Vite!' } }}</script> |
| 使用 Vue CLI(可选) | Vue CLI 是 Vue 官方的传统脚手架工具,功能完整但启动较慢,适合已有 CLI 项目或需要特定插件的场景。 | 1. 全局安装 CLI:npm install -g @vue/cli2. 创建项目: vue create my-vue-project3. 选择 Vue 3 预设或手动配置 4. 启动项目: cd my-vue-projectnpm run serve | src/main.js 示例:jsimport { createApp } from 'vue'import App from './App.vue'createApp(App).mount('#app') |
项目结构解析(以 Vite + Vue 项目为例)
| 目录/文件 | 作用说明 |
|---|---|
index.html | 入口 HTML 文件,Vue 应用挂载点 <div id="app"></div> |
package.json | 项目元信息,包含依赖和脚本命令(如 dev, build) |
vite.config.js | Vite 配置文件,可自定义服务器、插件等 |
src/ | 源码目录 |
src/main.js | 应用入口,创建 Vue 实例并挂载到 DOM |
src/App.vue | 根组件,通常包含路由视图或主布局 |
src/components/ | 存放可复用的 Vue 组件 |
src/assets/ | 静态资源(图片、CSS 等) |
public/ | 公共资源目录(如 favicon.ico),直接映射到根路径 |
views/ | 存放页面级组件(路由组件) |
router/ | 路由管理,使用 vue-router |
store/ | 状态管理,使用 vuex |
ASCII 图示:Vite 项目结构
my-vue-app/
├── index.html
├── vite.config.js
├── package.json
└── src/
├── main.js
├── App.vue
├── components/
│ └── HelloWorld.vue
├── views/
│ ├── HomeView.vue
│ └── AboutView.vue
├── router/
│ └── index.js
└── store/
└── index.js
💡 提示:Vite 项目启动后,默认在
http://localhost:5173提供开发服务器,保存代码后自动刷新。
3. 第一个 Vue 应用
| 文件 | 说明 | 最小可运行代码示例 |
|---|---|---|
index.html | 项目根目录 | 见下方代码块 |
src/main.js | 入口文件 | 见下方代码块 |
src/App.vue | 根组件 | 见下方代码块 |
src/components/HelloWorld.vue | 组件文件 | 见下方代码块 |
src/router/index.js | 路由配置 | 见下方代码块 |
src/store/index.js | 状态管理 | 见下方代码块 |
src/views/HomeView.vue | 页面文件 | 见下方代码块 |
src/views/AboutView.vue | 页面文件 | 见下方代码块 |
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue Minimal App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from "@/router/index.js";
import pinia from "@/store/index.js";
// 创建应用实例
const app = createApp(App)
// 使用插件
app.use(router)
app.use(pinia)
app.use(ElementPlus)
// 挂载应用
app.mount('#app')
src/App.vue
<template>
<div id="app">
<el-container>
<el-header>
<h1>Vue Minimal App</h1>
<el-menu :router="true" mode="horizontal" default-active="/">
<el-menu-item index="/">Home</el-menu-item>
<!-- 或者使用router-link标签 -->
<!-- <router-link to="/">Home</router-link> -->
<el-menu-item index="/about">About</el-menu-item>
<!-- <router-link to="/about">About</router-link> -->
</el-menu>
</el-header>
<el-main>
<!-- 路由视图 -->
<router-view />
</el-main>
</el-container>
</div>
</template>
<script setup>
// 这里可以写组合式 API 逻辑
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
}
</style>
src/components/HelloWorld.vue
<template>
<!-- 模板语法:插值 -->
<div>
<h2>{{ title }}</h2>
<!-- 模板语法:v-bind -->
<p :class="dynamicClass">This is a dynamic class.</p>
<!-- 模板语法:v-on -->
<button @click="increment">Count: {{ count }}</button>
<!-- 模板语法:v-if -->
<p v-if="showMessage">Welcome message!</p>
<p v-else>Message hidden.</p>
<!-- 模板语法:v-for -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<button @click="toggleMessage">Toggle Message</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// 响应式数据初探:ref 与 reactive
const count = ref(0)
const title = ref('Hello World')
function increment() {
count.value++
}
const dynamicClass = ref('highlight')
const showMessage = ref(true)
function toggleMessage() {
showMessage.value = !showMessage.value
}
const items = reactive([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
</script>
<style>
.highlight {
color: green;
}
</style>
src/router/index.js
// 创建路由
import {createRouter, createWebHistory} from "vue-router";
import HomeView from "@/views/HomeView.vue";
import AboutView from "@/views/AboutView.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
{ path: '/about', component: AboutView }
]
})
export default router;
src/store/index.js
// 创建 store
import {createPinia} from "pinia";
const pinia = createPinia()
export default pinia;
src/views/HomeView.vue
<template>
<div>
<h2>Home Page</h2>
<HelloWorld />
</div>
</template>
<script setup>
import HelloWorld from '../components/HelloWorld.vue'
</script>
src/views/AboutView.vue
<template>
<div>
<h2>About Page</h2>
<p>This is the about page.</p>
</div>
</template>
关键语法点表格
| 语法类别 | 具体语法 | 代码示例 | 文件位置 |
|---|---|---|---|
createApp 函数 | 创建应用实例 | import { createApp } from 'vue'const app = createApp(App) | src/main.js |
挂载应用:app.mount('#app') | 将应用挂载到 DOM | app.mount('#app') | src/main.js |
响应式数据初探:ref 与 reactive | ref 创建响应式基本类型 | const count = ref(0)count.value++ | src/components/HelloWorld.vue |
reactive 创建响应式对象 | const items = reactive([{ id: 1, name: 'Item 1' }]) | src/components/HelloWorld.vue | |
| 模板语法:插值 | 文本插值 {{ }} | {{ title }}{{ count }} | src/components/HelloWorld.vue |
模板语法:指令 - v-bind | 绑定属性 | :class="dynamicClass" | src/components/HelloWorld.vue |
模板语法:指令 - v-on | 绑定事件 | @click="increment"@click="toggleMessage" | src/components/HelloWorld.vue |
模板语法:指令 - v-if | 条件渲染 | v-if="showMessage"v-else | src/components/HelloWorld.vue |
模板语法:指令 - v-for | 列表渲染 | v-for="item in items" :key="item.id" | src/components/HelloWorld.vue |
ASCII 图示:Vue 应用结构关系
+---------------------+
| index.html |
| <div id="app"> |
+----------+----------+
|
v
+---------------------+
| createApp({...}) |
| setup() |
| +---------------+ |
| | ref(), | |
| | reactive(), | |
| | computed() | |
| +---------------+ |
+----------+----------+
|
v
+---------------------+
| 响应式数据模型 |
| count, tasks... |
+----------+----------+
|
v
+---------------------+
| 模板指令渲染 |
| {{ }}, v-if, v-for |
+---------------------+
💡 提示:
setup()是 Composition API 的入口,在组件创建前执行,用于定义响应式数据和逻辑。返回的对象中的属性可在模板中直接使用。
第二部分:模板与指令
4. 模板语法
| 语法 | 说明 | 最小可用代码示例 |
|---|---|---|
文本插值:{{ }} | 使用双大括号将 JavaScript 表达式的结果插入文本内容中。表达式在当前组件上下文中求值。 | 见下方代码块 |
原始 HTML:v-html | 将数据作为 HTML 渲染,而非纯文本。注意:有 XSS 风险,仅用于可信内容。 | 见下方代码块 |
属性绑定:v-bind 或 : | 动态绑定 HTML 属性(如 src, href, class, id 等)。v-bind: 可简写为 :。 | 见下方代码块 |
事件监听:v-on 或 @ | 绑定 DOM 事件监听器。触发时执行方法。v-on: 可简写为 @。 | 见下方代码块 |
表单输入绑定:v-model(双向绑定) | 在表单元素上创建双向数据绑定。当用户输入时,数据自动更新;数据变化时,视图也更新。 | 见下方具体用法 |
文本插值:{{ }}
<p>消息: {{ message }}</p>
<p>计算: {{ count + 1 }}</p>
<p>三元运算: {{ ok ? '是' : '否' }}</p>
setup() {
const message = ref('Hello');
const count = ref(5);
const ok = ref(true);
return { message, count, ok };
}
原始 HTML:v-html
<div v-html="rawHtml"></div>
setup() {
const rawHtml = ref('<span style="color: red;">红色文本</span>');
return { rawHtml };
}
属性绑定:v-bind 或 :
<img v-bind:src="imageSrc" />
<a :href="website">访问官网</a>
<div :class="isActive ? 'active' : 'normal'"></div>
<button :disabled="isButtonDisabled">按钮</button>
setup() {
const imageSrc = ref('https://vuejs.org/images/logo.png');
const website = ref('https://vuejs.org');
const isActive = ref(true);
const isButtonDisabled = ref(true);
return { imageSrc, website, isActive, isButtonDisabled };
}
事件监听:v-on 或 @
<button v-on:click="handleClick">点击我</button>
<button @click="increment">+1</button>
<input @input="handleInput" placeholder="输入..." />
<form @submit.prevent="onSubmit">...</form>
setup() {
const count = ref(0);
const handleClick = () => alert('被点击了!');
const increment = () => count.value++;
const handleInput = (e) => console.log(e.target.value);
const onSubmit = () => console.log('表单提交');
return { count, handleClick, increment, handleInput, onSubmit };
}
v-model 基础用法
input(文本):
<input v-model="text" placeholder="输入文本" />
<p>你输入的是: {{ text }}</p>
setup() {
const text = ref('');
return { text };
}
textarea:
<textarea v-model="message" placeholder="请输入..."></textarea>
<p>内容: {{ message }}</p>
setup() {
const message = ref('');
return { message };
}
select:
<select v-model="selected">
<option value="">请选择</option>
<option value="vue">Vue</option>
<option value="react">React</option>
<option value="angular">Angular</option>
</select>
<p>你选择了: {{ selected }}</p>
setup() {
const selected = ref('');
return { selected };
}
v-model 修饰符
| 修饰符 | 说明 | 代码示例 |
|---|---|---|
.lazy | 将 v-model 的同步时机从 input 事件改为 change 事件(即输入框失去焦点时才更新数据)。 | <input v-model.lazy="text" /><p>延迟更新: {{ text }}</p> |
.number | 自动将输入值转换为数字类型。如果无法解析,则返回原始字符串。 | <input v-model.number="age" type="number" /><p>年龄(类型): {{ typeof age }}</p>输入 25 → age 为 number 类型 |
.trim | 自动过滤输入值的首尾空格。 | <input v-model.trim="username" /><p>用户名(已去空格): "{{ username }}"</p> |
综合示例:表单完整用法
<form @submit.prevent="submitForm">
<div>
<label>姓名:</label>
<input v-model.trim="form.name" />
</div>
<div>
<label>年龄:</label>
<input v-model.number="form.age" type="number" />
</div>
<div>
<label>自我介绍:</label>
<textarea v-model.lazy="form.bio"></textarea>
</div>
<div>
<label>技术栈:</label>
<select v-model="form.framework">
<option value="">选择框架</option>
<option value="vue">Vue</option>
<option value="react">React</option>
</select>
</div>
<button type="submit">提交</button>
</form>
<pre>{{ form }}</pre>
setup() {
const form = reactive({
name: '',
age: 0,
bio: '',
framework: ''
});
const submitForm = () => {
console.log('表单数据:', form);
};
return { form, submitForm };
}
ASCII 图示:v-model 双向绑定数据流
+---------------------+
| 用户输入 |
| (Input Event) |
+----------+----------+
|
v
+---------------------+
| <input v-model> |
+----------+----------+
|
v
+---------------------+
| 数据模型 (ref) |
| text.value = "..."|
+----------+----------+
|
v
+---------------------+
| 模板渲染 |
| {{ text }} |
+---------------------+
双向箭头表示:用户输入 ↔ 数据更新 ↔ 视图刷新
💡 提示:
v-model本质上是:value和@input的语法糖。- 修饰符可链式使用,如
v-model.trim.number。- 在组件上使用
v-model时,可通过defineModel()(Vue 3.4+)简化实现。
5. 条件渲染
| 指令 | 说明 | 最小可用代码示例 |
|---|---|---|
v-if | 条件性地渲染元素。如果表达式为”假值”,元素不会存在于 DOM 中。适合控制大块内容的显示。 | 见下方代码块 |
v-else | 必须紧跟在 v-if 或 v-else-if 之后,表示”否则”的情况。没有对应的条件表达式。 | 见下方代码块 |
v-else-if | 表示多个条件分支,必须紧跟在 v-if 或另一个 v-else-if 之后。 | 见下方代码块 |
v-show | 始终渲染元素,但通过 CSS 的 display: none 控制显隐。元素始终存在于 DOM 中。 | 见下方代码块 |
v-if / v-else / v-else-if
<div v-if="isVisible">这个元素可能不显示</div>
<div v-if="type === 'A'">类型 A</div>
<div v-else>其他类型</div>
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 80">良好</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
setup() {
const isVisible = ref(true);
const type = ref('A');
const score = ref(85);
return { isVisible, type, score };
}
v-show
<div v-show="isVisible">这个元素始终在 DOM 中</div>
setup() {
const isVisible = ref(true);
return { isVisible };
}
v-if vs v-show 对比
| 对比项 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件满足时才渲染到 DOM(惰性) | 始终渲染,通过 display: none 切换 |
| 初始渲染开销 | 高(可能跳过) | 始终渲染,初始开销固定 |
| 切换开销 | 高(频繁增删 DOM) | 低(仅切换 CSS) |
| 适用场景 | 条件很少改变 | 频繁切换显隐 |
能否与 v-else 配合 | ✅ 可以 | ❌ 不支持 |
支持 <template> | ✅ 支持 | ❌ 不支持 |
<template> 标签的使用
<template> 是一个不可见的包装元素,用于组合多个元素进行条件渲染或列表渲染,避免引入额外的 DOM 节点。
| 场景 | 说明 | 示例代码 |
|---|---|---|
多元素 v-if | 当需要同时控制多个元素的显示时,使用 <template> 包裹,避免添加无意义的父容器。 | 见下方代码块 |
| 避免额外包裹 | 使用 <template> 可避免因条件渲染而引入不必要的 <div>,保持结构干净。 | 见下方代码块 |
多元素 v-if:
<template v-if="user.loggedIn">
<h2>欢迎, {{ user.name }}</h2>
<p>你的邮箱: {{ user.email }}</p>
<button>登出</button>
</template>
<div v-else>
<p>请先登录</p>
</div>
setup() {
const user = reactive({
loggedIn: true,
name: 'Alice',
email: 'alice@example.com'
});
return { user };
}
避免额外包裹:
<ul>
<template v-for="item in items" :key="item.id">
<li v-if="item.active">{{ item.name }}</li>
<li v-else class="inactive">{{ item.name }}</li>
</template>
</ul>
综合示例:条件渲染完整用法
注意:同一个
.vue文件中,只能存在一个顶层<template>标签。
<template>
<div>
<button @click="toggleLogin">切换登录状态</button>
<button @click="changeRole">切换角色</button>
</div>
<!-- 使用 v-if / v-else-if / v-else -->
<template v-if="role === 'admin'">
<h3>管理员面板</h3>
<p>你可以管理所有用户。</p>
</template>
<template v-else-if="role === 'editor'">
<h3>编辑面板</h3>
<p>你可以编辑文章。</p>
</template>
<template v-else>
<h3>读者面板</h3>
<p>你可以阅读文章。</p>
</template>
<!-- 使用 v-show -->
<div v-show="showSettings">
<h4>设置选项</h4>
<p>这里是设置内容。</p>
</div>
<button @click="showSettings = !showSettings">
{{ showSettings ? '隐藏' : '显示' }} 设置
</button>
</template>
<script>
setup() {
const role = ref('admin');
const showSettings = ref(true);
const toggleLogin = () => {
// 模拟切换角色
const roles = ['admin', 'editor', 'reader'];
const currentIndex = roles.indexOf(role.value);
role.value = roles[(currentIndex + 1) % roles.length];
};
const changeRole = () => {
// 另一种切换方式
role.value = role.value === 'admin' ? 'editor' : 'admin';
};
return { role, showSettings, toggleLogin, changeRole };
}
</script>
ASCII 图示:v-if 与 v-show 渲染机制差异
v-if 渲染机制:
+------------------+
| 条件: true |
+------------------+
|
v
+------------------+
| 元素被插入 DOM |
+------------------+
+------------------+
| 条件: false |
+------------------+
|
v
+------------------+
| 元素不存在于 DOM |
+------------------+
v-show 渲染机制:
+------------------+
| 条件: true |
+------------------+
|
v
+------------------+
| 元素存在 |
| style="" |
+------------------+
+------------------+
| 条件: false |
+------------------+
|
v
+------------------+
| 元素存在 |
| style="display: none" |
+------------------+
💡 提示:
v-if有更高的切换开销,v-show有更高的初始渲染开销。v-if支持<template>和v-else,v-show不支持。- 频繁切换推荐
v-show,运行时条件很少改变推荐v-if。
6. 列表渲染
| 指令/概念 | 说明 | 最小可用代码示例 |
|---|---|---|
v-for 遍历数组 | 使用 v-for 指令遍历数组,语法为 (item, index) in items。index 可选。 | 见下方代码块 |
v-for 遍历对象 | 遍历对象的属性,语法为 (value, key, index) in object。index 可选。 | 见下方代码块 |
key 的重要性 | key 是 Vue 用于追踪每个节点身份的特殊属性。它帮助 Vue 重用和重新排序现有元素,提高渲染性能。 | 见下方代码块 |
| 变异方法(Mutation Methods) | Vue 能检测以下变异方法对数组的修改,并触发视图更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse() | 见下方代码块 |
| 非变异方法(Non-mutating Methods) | 如 filter(), concat(), slice() 等方法不修改原数组,而是返回新数组。需将结果赋值给原引用才能触发更新。 | 见下方代码块 |
v-for 遍历数组
<ul>
<li v-for="(user, index) in users" :key="index">
{{ index }}. {{ user.name }} - {{ user.age }}岁
</li>
</ul>
setup() {
const users = ref([
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
]);
return { users };
}
v-for 遍历对象
<ul>
<li v-for="(value, key, index) in profile" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
setup() {
const profile = ref({
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
});
return { profile };
}
key 的重要性
- 推荐使用唯一且稳定的值(如 ID),避免使用 index(仅在列表静态且无顺序变化时可用)。
- 缺少
key会导致状态错乱(如输入框值错位)。
<!-- 推荐:使用唯一 ID -->
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
<!-- 不推荐:使用 index -->
<li v-for="(user, index) in users" :key="index">
{{ user.name }}
</li>
变异方法(Mutation Methods)
// 这些操作会触发更新
users.value.push({ id: 4, name: 'David', age: 28 });
users.value.splice(1, 1); // 删除索引1的元素
users.value.sort((a, b) => a.age - b.age);
非变异方法(Non-mutating Methods)
// ❌ 错误:不触发更新
users.value.filter(u => u.age > 30);
// ✅ 正确:赋值新数组
users.value = users.value.filter(u => u.age > 30);
响应式替换数组
当需要替换整个数组时(如搜索结果),直接赋值新数组即可,Vue 会检测到变化。
// 模拟搜索
const search = (keyword) => {
users.value = originalUsers.filter(u =>
u.name.includes(keyword)
);
};
列表过滤与排序
| 方法 | 说明 | 示例代码 |
|---|---|---|
| 计算属性实现过滤 | 使用 computed 创建过滤后的列表,保持原始数据不变。 | 见下方代码块 |
| 计算属性实现排序 | 结合 sort() 方法对列表进行排序。 | 见下方代码块 |
| 组合过滤与排序 | 可链式使用 computed 实现复杂逻辑。 | 见下方代码块 |
计算属性实现过滤:
<input v-model="searchQuery" placeholder="搜索姓名..." />
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
setup() {
const searchQuery = ref('');
const users = ref([...]); // 原始数据
const filteredUsers = computed(() =>
users.value.filter(u =>
u.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
);
return { searchQuery, filteredUsers };
}
计算属性实现排序:
const sortedUsers = computed(() =>
[...users.value].sort((a, b) => a.age - b.age)
);
// 注意:使用 [...array] 创建副本,避免修改原数组
组合过滤与排序:
const processedUsers = computed(() => {
let result = users.value;
// 过滤
if (searchQuery.value) {
result = result.filter(u =>
u.name.includes(searchQuery.value)
);
}
// 排序
return result.sort((a, b) => a.age - b.age);
});
综合示例:可搜索与排序的用户列表
<div>
<input
v-model="searchQuery"
placeholder="搜索用户..."
/>
<button @click="sortByAge">按年龄排序</button>
</div>
<ul>
<li v-for="user in processedUsers" :key="user.id">
{{ user.name }} ({{ user.age }}岁)
</li>
</ul>
import { ref, computed } from 'vue';
setup() {
const searchQuery = ref('');
const sortAsc = ref(true);
const users = ref([
{ id: 1, name: 'Charlie', age: 35 },
{ id: 2, name: 'Alice', age: 25 },
{ id: 3, name: 'Bob', age: 30 }
]);
const processedUsers = computed(() => {
let filtered = users.value.filter(user =>
user.name.includes(searchQuery.value)
);
return filtered.sort((a, b) =>
sortAsc.value ? a.age - b.age : b.age - a.age
);
});
const sortByAge = () => {
sortAsc.value = !sortAsc.value;
};
return {
searchQuery,
processedUsers,
sortByAge
};
}
ASCII 图示:v-for 渲染与 key 的作用
没有 key(使用 index):
+----------------+ +----------------+
| 初始状态 | | 更新后状态 |
| [Alice, Bob] | | [David, Alice] |
| 0: Alice | | 0: David |
| 1: Bob | | 1: Alice |
+----------------+ +----------------+
↓ 问题:Vue 认为 ↓ 实际应为
"0号还是Alice" "0号是David,1号是Alice"
"1号还是Bob" 但 Vue 可能复用错误的DOM节点
有 key(使用 id):
+----------------+ +----------------+
| 初始状态 | | 更新后状态 |
| {id:1,Alice} | | {id:4,David} |
| {id:2,Bob} | | {id:1,Alice} |
+----------------+ +----------------+
↓ Vue 根据 key 追踪
"id=1 的节点移动到位置1"
"新增 id=4 的节点"
"移除 id=2 的节点"
→ 正确更新,避免状态错乱
💡 提示:
- 始终为
v-for提供key,优先使用数据的唯一 ID。- 避免使用数组索引作为
key,尤其在列表可能发生排序、过滤或中间插入/删除时。- 对于大数据列表,可结合虚拟滚动(如
vue-virtual-scroller)优化性能。
第三部分:组件化开发
7. 组件基础
| 概念 | 说明 | 最小可用代码示例 |
|---|---|---|
| 什么是组件?组件化思想 | 组件是可复用的 Vue 实例,用于封装 UI 和逻辑。组件化思想将页面拆分为独立、可维护的小块(如按钮、卡片、导航栏),实现:复用性、可维护性、职责分离。 | 见下方代码块 |
定义组件:defineComponent(可选) | defineComponent 是一个辅助函数,主要用于 TypeScript 支持和 IDE 类型推断。在 <script setup> 中非必需,但在普通 <script> 中推荐使用。 | 见下方代码块 |
| 注册组件:局部注册 | 在父组件中通过 components 选项注册子组件,仅在当前组件内可用。适合按需加载和模块化开发。 | 见下方代码块 |
| 注册组件:全局注册 | 通过 app.component() 在应用实例上注册组件,可在任何组件模板中直接使用。适合高频使用的通用组件(如按钮、弹窗)。 | 见下方代码块 |
使用组件:<MyComponent /> | 在模板中通过标签名使用已注册的组件。组件名遵循 PascalCase 或 kebab-case 命名规范。 | 见下方代码块 |
什么是组件?组件化思想
<!-- 页面由多个组件构成 -->
<Header />
<MainContent>
<Sidebar />
<ArticleList />
</MainContent>
<Footer />
定义组件:defineComponent(可选)
<!-- components/UserCard.vue -->
<template>
<div class="card">
<h3>{{ name }}</h3>
<p>年龄: {{ age }}</p>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'UserCard',
props: {
name: String,
age: Number
}
});
</script>
注册组件:局部注册
<!-- ParentComponent.vue -->
<template>
<div>
<h2>用户列表</h2>
<UserCard name="Alice" :age="25" />
<UserCard name="Bob" :age="30" />
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UserCard from './components/UserCard.vue';
export default defineComponent({
components: {
UserCard // 局部注册
}
});
</script>
注册组件:全局注册
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import Button from './components/Button.vue';
const app = createApp(App);
app.component('Button', Button); // 全局注册
app.mount('#app');
之后可在任意 .vue 文件中使用:
<template>
<Button>点击我</Button>
</template>
使用组件:<MyComponent />
<template>
<!-- 使用局部或全局注册的组件 -->
<UserCard name="Charlie" :age="35" />
<Modal title="提示" v-model="showModal">
这是一个模态框。
</Modal>
<CustomButton @click="handleClick" />
</template>
综合示例:父子组件通信雏形
components/CounterDisplay.vue:
<template>
<div class="counter">
<strong>计数器:</strong> {{ count }}
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'CounterDisplay',
props: {
count: {
type: Number,
required: true
}
}
});
</script>
Parent.vue:
<template>
<div>
<h1>主组件</h1>
<!-- 使用局部注册的子组件 -->
<CounterDisplay :count="currentCount" />
<button @click="currentCount++">+1</button>
<button @click="currentCount--">-1</button>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import CounterDisplay from './components/CounterDisplay.vue';
export default defineComponent({
name: 'Parent',
components: {
CounterDisplay
},
setup() {
const currentCount = ref(0);
return { currentCount };
}
});
</script>
ASCII 图示:组件树结构
+---------------------+
| App (根组件) |
+----------+----------+
|
v
+---------------------+
| ParentComponent |
| +---------------+ |
| | components: | |
| | CounterDisplay | |
| +---------------+ |
+----------+----------+
|
+-----+-----+
| |
v v
+-----------+ +-----------+
|CounterDis-| |CounterDis-|
| play | | play |
+-----------+ +-----------+
说明:
- 虚线框:组件注册范围
- 实线箭头:组件使用关系
- 数据流:props 向下,events 向上(本节未展开)
💡 提示:
defineComponent在 Vue 3 中主要是类型工具,JavaScript 项目中可省略,但强烈推荐使用以保持一致性。- 局部注册更利于代码分割和 tree-shaking;全局注册方便但可能增加包体积。
- 组件文件通常放在
components/目录下,命名推荐使用 PascalCase(如UserProfile.vue)。- 在
<script setup>语法中,导入的组件自动局部注册,无需写components选项。
8. 组件通信
| 通信方式 | 说明 | 最小可用代码示例 |
|---|---|---|
| 父传子:props | 父组件通过属性向子组件传递数据。子组件使用 props 选项声明接收的属性,数据流为单向向下。 | 见下方代码块 |
| Props 类型校验(TypeScript) | 使用 TypeScript 为 props 提供静态类型检查,提升开发体验和代码健壮性。 | 见下方代码块 |
| Props 默认值、必填项 | 可为 props 设置默认值和是否必填。使用对象语法定义 props 时支持。 | 见下方代码块 |
| 子传父:emit 事件 | 子组件通过 $emit(选项式)或 emit(组合式)触发事件,父组件通过 v-on 监听并响应。实现数据向上流动。 | 见下方代码块 |
defineEmits 定义事件 | 在 <script setup> 中声明组件会触发的事件,支持类型推导(TS)和运行时验证。 | 见下方代码块 |
| 非父子通信:provide / inject | 祖先组件通过 provide 提供数据,后代组件通过 inject 注入数据。跨层级传递,避免”props 逐层透传”。 | 见下方代码块 |
| 事件总线(mitt 库) | 使用第三方库 mitt 创建全局事件总线,实现任意组件间通信。适合松耦合场景,但应避免滥用。 | 见下方代码块 |
$attrs / $slots | $attrs:包含未被 props 声明的属性和事件监听器,可用于透传到子组件。$slots:用于接收和渲染插槽内容,实现内容分发。 | 见下方代码块 |
父传子:props
Parent.vue:
<template>
<ChildComponent message="来自父组件" :count="5" />
</template>
ChildComponent.vue:
<template>
<div>
<p>{{ message }}</p>
<p>数量: {{ count }}</p>
</div>
</template>
<script setup>
const props = defineProps(['message', 'count']);
</script>
Props 类型校验(TypeScript)
<script setup lang="ts">
type Props = {
message: string;
count?: number; // 可选
isActive?: boolean;
};
const props = defineProps<Props>();
</script>
Props 默认值、必填项
<script setup lang="ts">
interface Props {
title: string;
enabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
title: '默认标题',
enabled: true
});
</script>
或 JavaScript 写法:
const props = defineProps({
title: { type: String, default: '默认标题' },
enabled: { type: Boolean, default: true }
});
子传父:emit 事件
ChildComponent.vue:
<template>
<button @click="notifyParent">通知父组件</button>
</template>
<script setup>
const emit = defineEmits(['notify']);
const notifyParent = () => { emit('notify', '子组件的消息'); };
</script>
Parent.vue:
<template>
<ChildComponent @notify="handleNotify" />
<p>收到: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('');
const handleNotify = (msg: string) => { message.value = msg; };
</script>
defineEmits 定义事件
<script setup lang="ts">
// TS 推荐写法
const emit = defineEmits<{
(e: 'update', id: number): void;
(e: 'close'): void;
}>();
const update = () => emit('update', 123);
const close = () => emit('close');
</script>
或运行时写法:
const emit = defineEmits(['update', 'close']);
非父子通信:provide / inject
父组件(提供者):
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark');
const changeTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
};
provide('theme', theme);
provide('changeTheme', changeTheme);
</script>
深层子组件(注入者):
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
const changeTheme = inject('changeTheme');
</script>
<template>
<div :class="theme">当前主题: {{ theme }}</div>
<button @click="changeTheme">切换主题</button>
</template>
事件总线(mitt 库)
events.ts:
import mitt from 'mitt';
export const emitter = mitt();
组件 A:发送事件:
<script setup>
import { emitter } from './events';
const sendMessage = () => {
emitter.emit('alert', '警告信息!');
};
</script>
组件 B:监听事件:
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { emitter } from './events';
const showAlert = (msg: string) => {
alert(msg);
};
onMounted(() => {
emitter.on('alert', showAlert);
});
onUnmounted(() => {
emitter.off('alert', showAlert);
});
</script>
$attrs / $slots
WrapperInput.vue:
<template>
<div class="wrapper">
<input v-bind="$attrs" />
</div>
</template>
<script setup>
// 无需 defineProps,所有属性自动透传
</script>
使用:
<WrapperInput v-model="text" @input="onInput" placeholder="请输入" />
ASCII 图示:组件通信方式概览
+---------------------+
| 事件总线 (mitt) |
| 全局通信,任意组件 |
+----------+----------+
|
v
+---------------------+
| provide / inject |
| 祖先 → 后代(深层) |
+----------+----------+
|
+-----+-----+
| |
v v
+-----------+ +-----------+
| 父组件 | | 子组件 |
| | | |
| props → | | |
| | | ← emit |
+-----------+ +-----------+
说明:
- → : props(父 → 子)
- ← : emit(子 → 父)
- ↕ : provide/inject(祖孙双向)
- ⚡ : 事件总线(任意组件)
- ↗ :
$attrs透传 - 📦 :
$slots插槽内容分发
💡 提示:
props和emit是最基础、最推荐的通信方式,符合单向数据流原则。provide/inject应用于依赖注入场景,如主题、配置、用户信息等。- 事件总线 (
mitt) 灵活但难以追踪,建议仅在没有更好方案时使用。$attrs和$slots常用于高阶组件或 UI 库组件封装。
9. 插槽(Slots)
| 插槽类型 | 说明 | 最小可用代码示例 |
|---|---|---|
| 默认插槽(Default Slot) | 子组件中使用 <slot></slot> 占位,父组件在使用时传入任意内容(文本、HTML、组件等),实现内容分发。当只有一个插槽时最常用。 | 见下方代码块 |
| 具名插槽(Named Slot) | 使用 name 属性为插槽命名,允许子组件定义多个插槽位置。父组件通过 v-slot:xxx 或 #xxx 向指定插槽传入内容。 | 见下方代码块 |
| 作用域插槽(Scoped Slot) | 子组件通过 slot 标签的属性将数据暴露给父组件,父组件使用 v-slot="slotProps" 接收这些数据,实现子 → 父的数据传递与自定义渲染逻辑。 | 见下方代码块 |
缩写语法:v-slot | v-slot: 可简写为 #。 | 见下方代码块 |
| 动态插槽名 | 使用方括号 [ ] 动态绑定插槽名,适用于需要根据变量决定内容插入位置的场景。 | 见下方代码块 |
默认插槽(Default Slot)
Card.vue:
<template>
<div class="card">
<header>卡片标题</header>
<main>
<slot></slot> <!-- 默认插槽位置 -->
</main>
</div>
</template>
父组件使用:
<template>
<Card>
<p>这是卡片的内容。</p>
<button>操作按钮</button>
</Card>
</template>
具名插槽(Named Slot)
Layout.vue:
<template>
<div class="layout">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>
父组件使用:
<template>
<Layout>
<template v-slot:header>
<h1>页面标题</h1>
</template>
<p>主内容区域</p>
<template #footer>
<small>© 2025 版权信息</small>
</template>
</Layout>
</template>
作用域插槽(Scoped Slot)
UserList.vue:
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- 将 user 数据暴露给父组件 -->
<slot :user="user" :index="$index">
<!-- 默认内容 -->
{{ user.name }}
</slot>
</li>
</ul>
</template>
<script setup>
const users = [
{ id: 1, name: 'Alice', email: 'a@ex.com' },
{ id: 2, name: 'Bob', email: 'b@ex.com' }
];
</script>
父组件:自定义渲染每个用户:
<template>
<UserList v-slot="slotProps">
<strong>{{ slotProps.user.name }}</strong>
<em>{{ slotProps.user.email }}</em>
</UserList>
</template>
缩写语法:v-slot
| 完整写法 | 简写 |
|---|---|
v-slot:header | #header |
v-slot:default | #default |
v-slot="{ user }" | #default="{ user }" |
<template>
<Layout>
<template #header>
<h1>标题</h1>
</template>
<template #default="{ user }">
自定义:{{ user.name }}
</template>
</Layout>
</template>
动态插槽名
DynamicLayout.vue:
<template>
<div>
<slot :name="currentSection"></slot>
<slot name="sidebar"></slot>
</div>
</template>
父组件:
<template>
<DynamicLayout>
<!-- 动态决定插入哪个具名插槽 -->
<template #[dynamicSlotName]>
<p>动态插槽内容</p>
</template>
</DynamicLayout>
</template>
<script setup>
const currentView = ref('article');
const dynamicSlotName = computed(() =>
currentView.value === 'home' ? 'header' : 'main'
);
</script>
综合示例:可定制的表格组件
DataTable.vue:
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td>{{ row.id }}</td>
<td>
<!-- 作用域插槽:允许父组件自定义姓名渲染 -->
<slot name="name" v-bind:row="row">
{{ row.name }}
</slot>
</td>
<td>
<!-- 作用域插槽:自定义操作按钮 -->
<slot name="actions" v-bind:row="row">
<button @click="$emit('edit', row)">编辑</button>
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps(['data']);
defineEmits(['edit']);
</script>
父组件使用:
<template>
<DataTable :data="users" @edit="onEdit">
<!-- 自定义姓名列:添加头像 -->
<template #name="{ row }">
<img :src="row.avatar" width="24" /> {{ row.name }}
</template>
<!-- 自定义操作列 -->
<template #actions="{ row }">
<button @click="edit(row)">修改</button>
<button @click="del(row)" class="danger">删除</button>
</template>
</DataTable>
</template>
<script setup>
import { ref } from 'vue';
const users = ref([
{ id: 1, name: 'Alice', avatar: '/a.jpg' },
{ id: 2, name: 'Bob', avatar: '/b.jpg' }
]);
const edit = (user) => { /*...*/ };
const del = (user) => { /*...*/ };
</script>
ASCII 图示:插槽工作原理
+-----------------------+
| 父组件 |
| |
| <ChildComponent> |
| <template #header> |
| <h1>标题</h1> | ← 内容
| </template> |
| |
| <p>主内容</p> | ← 默认插槽内容
| |
| <template #footer> |
| © 2025 |
| </template> |
| </ChildComponent> |
+----------+------------+
|
v (内容分发)
+-----------------------+
| ChildComponent |
| |
| <header> |
| <slot name="header">|
| ↓ 接收父组件内容 |
| <h1>标题</h1> |
| </slot> |
| </header> |
| |
| <main> |
| <slot> |
| ↓ 接收默认内容 |
| <p>主内容</p> |
| </slot> |
| </main> |
| |
| <footer> |
| <slot name="footer">|
| ↓ 接收底部内容 |
| © 2025 |
| </slot> |
| </footer> |
+-----------------------+
作用域插槽数据流:子组件 → (暴露数据) → 父组件,允许父组件基于子组件数据自定义模板。
💡 提示:
- 默认插槽的
name是"default",可省略。v-slot只能用于<template>或组件标签上。- 作用域插槽是实现高可定制组件(如表格、列表、弹窗)的核心技术。
- 动态插槽名结合计算属性可实现灵活的布局系统。
第四部分:响应式与 Composition API
10. Composition API 核心
| 概念 | 说明 | 最小可用代码示例 |
|---|---|---|
setup() 函数执行时机 | setup() 是 Composition API 的入口函数,在组件创建之前执行,早于 beforeCreate 钩子。接收 props 和 context 参数,不能访问 this,返回的对象/函数将暴露给模板和其他选项。 | 见下方代码块 |
ref():创建响应式基本类型 | 用于包装基本类型(string, number, boolean, null, undefined)或对象,使其具有响应性。访问值需通过 .value,在模板中使用时自动解包(无需 .value)。 | 见下方代码块 |
reactive():创建响应式对象 | 用于创建一个响应式对象。直接操作属性即可触发更新。不能用于基本类型,深层响应式(嵌套属性也响应),替代 data() 选项。 | 见下方代码块 |
computed():计算属性 | 创建一个可缓存的响应式值,基于其他响应式数据计算得出。有缓存机制,依赖不变时不重新计算。默认为 getter,可提供 setter。 | 见下方代码块 |
watch():侦听器 | 显式侦听一个或多个响应式数据源,并在其变化时执行回调。支持侦听 ref、reactive、computed 或 getter 函数。可配置 deep(深度监听)、immediate(立即执行)。 | 见下方代码块 |
watchEffect():自动依赖追踪侦听器 | 立即执行传入的函数,并自动追踪其内部访问的响应式属性作为依赖。无需指定 source,依赖变化时重新执行,默认 immediate: true。 | 见下方代码块 |
setup() 函数
<script>
import { ref } from 'vue';
export default {
props: ['title'],
setup(props, context) {
console.log(props.title); // 可访问 props
const count = ref(0);
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
ref():创建响应式基本类型
<script setup>
import { ref } from 'vue';
const count = ref(0);
const name = ref('Vue');
const increment = () => {
count.value++;
};
</script>
<template>
<!-- 模板中自动解包,无需 .value -->
<p>{{ count }}</p>
<p>你好,{{ name }}</p>
<button @click="increment">+1</button>
</template>
reactive():创建响应式对象
<script setup>
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
}
});
const increment = () => {
state.count++;
};
const updateName = (newName) => {
state.user.name = newName;
};
</script>
<template>
<p>计数: {{ state.count }}</p>
<p>姓名: {{ state.user.name }}</p>
<button @click="increment">+1</button>
<input :value="state.user.name" @input="e => updateName(e.target.value)" />
</template>
computed():计算属性
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读计算属性
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});
// 可写计算属性
const fullNameWritable = computed({
get() {
return firstName.value + ' ' + lastName.value;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});
</script>
<template>
<p>全名: {{ fullName }}</p>
<input v-model="fullNameWritable" />
</template>
watch():侦听器
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const user = ref({ name: 'Alice', age: 25 });
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 变化: ${oldVal} → ${newVal}`);
});
// 侦听 reactive 对象(默认浅层)
watch(user, (newVal, oldVal) => {
console.log('user 变化', newVal);
}, { deep: true });
// 侦听 getter 函数
watch(() => user.value.name, (newName) => {
console.log('名字变了:', newName);
});
// 立即执行
watch(count, (newVal) => {
console.log('立即执行:', newVal);
}, { immediate: true });
</script>
watchEffect():自动依赖追踪侦听器
<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('Vue');
// 自动追踪 count 和 name
watchEffect(() => {
console.log(`watchEffect: ${name.value} 的计数是 ${count.value}`);
});
// 执行后立即输出:
// "watchEffect: Vue 的计数是 0"
// 当 count 或 name 变化时再次执行
</script>
深度监听(deep)与立即执行(immediate)
const user = ref({ profile: { name: 'Alice' } });
// ❌ 不会触发(仅监听 user 引用)
watch(user, () => console.log('changed'));
// ✅ 会触发(深度监听)
watch(user, () => console.log('changed'), { deep: true });
// 立即执行
watch(count, (val) => {
console.log('当前值:', val); // 立即输出初始值
}, { immediate: true });
ASCII 图示:Composition API 响应式系统概览
+------------------+
| setup() |
| 执行时机:组件 |
| 创建前,早于 |
| beforeCreate |
+--------+---------+
|
v
+-------------+-------------+
| |
v v
+------------+ +------------------+
| ref() | | reactive() |
| 基本类型 | | 对象类型 |
| .value 访问 | | 直接属性操作 |
+-----+------+ +--------+---------+
| |
| +------------------v------------------+
| | computed() |
| | 基于 ref/reactive 计算 |
| | 有缓存,依赖变化才重新计算 |
| +------------------+------------------+
| |
+-------------+-------------+
|
v
+----------+-----------+
| watch() |
| 显式侦听数据源 |
| 可配置 deep/immediate|
+----------+-----------+
|
v
+----------+-----------+
| watchEffect() |
| 自动追踪依赖 |
| 立即执行,重新运行 |
+----------------------+
数据流方向:↑ 响应式数据创建 → → → → → → → → → → ↓,↑ 依赖追踪与更新
💡 提示:
<script setup>是 Composition API 的语法糖,自动执行setup()。- 优先使用
ref,即使在reactive中也推荐用ref包装基本类型以保持响应性。watchEffect简洁但不易控制依赖,适合简单副作用;watch更明确,适合复杂逻辑。- 避免过度使用
deep: true,可能影响性能,优先考虑更精确的侦听路径。
11. 生命周期钩子
Composition API 生命周期钩子
| Composition API 钩子 | 对应 Options API | 说明 | 最小可用代码示例 |
|---|---|---|---|
onBeforeMount | beforeMount | 在组件挂载(插入 DOM 树)之前调用。此时模板尚未渲染,不能访问 $el。 | 见下方代码块 |
onMounted | mounted | 在组件挂载完成后调用。可安全访问 DOM($el)。常用于:发起网络请求、设置定时器、初始化第三方库。 | 见下方代码块 |
onBeforeUpdate | beforeUpdate | 在组件数据更新、重新渲染之前调用。此时 DOM 还是旧的,可访问之前的 DOM 状态。 | 见下方代码块 |
onUpdated | updated | 在组件更新、重新渲染之后调用。DOM 已更新,可访问新的 DOM 状态。⚠️ 注意避免在此钩子中修改状态,可能引发无限循环。 | 见下方代码块 |
onBeforeUnmount | beforeDestroy (Vue 2) / beforeUnmount (Vue 3) | 在组件卸载(从 DOM 移除)之前调用。组件仍完全运行中。常用于:清除定时器、解绑事件、销毁第三方实例。 | 见下方代码块 |
onUnmounted | destroyed (Vue 2) / unmounted (Vue 3) | 在组件卸载后调用。组件实例已被销毁,所有指令、事件监听器、子组件均被移除,不能访问组件数据或方法。 | 见下方代码块 |
onErrorCaptured | errorCaptured | 当捕获到后代组件的错误时调用。可返回 false 阻止错误继续向上抛出,常用于错误日志上报。 | 见下方代码块 |
onBeforeMount
<script setup>
import { onBeforeMount } from 'vue';
onBeforeMount(() => {
console.log('组件即将挂载');
});
</script>
onMounted
<script setup>
import { onMounted, ref } from 'vue';
const count = ref(0);
onMounted(() => {
console.log('组件已挂载');
console.log('DOM 已存在:', document.querySelector('button'));
// 示例:挂载后开始计时
setInterval(() => {
count.value++;
}, 1000);
});
</script>
<template>
<button>{{ count }}</button>
</template>
onBeforeUpdate
<script setup>
import { ref, onBeforeUpdate } from 'vue';
const count = ref(0);
onBeforeUpdate(() => {
console.log('数据将更新,当前 DOM 值:',
document.querySelector('p')?.textContent);
});
const increment = () => {
count.value++;
};
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</template>
onUpdated
<script setup>
import { ref, onUpdated } from 'vue';
const list = ref(['A']);
onUpdated(() => {
console.log('列表已更新,当前项数:', list.value.length);
});
const addItem = () => {
list.value.push(String.fromCharCode(65 + list.value.length));
};
</script>
<template>
<ul><li v-for="item in list" :key="item">{{ item }}</li></ul>
<button @click="addItem">添加</button>
</template>
onBeforeUnmount 与 onUnmounted
<script setup>
import { ref, onMounted, onBeforeUnmount, onUnmounted } from 'vue';
const timerId = ref(null);
const count = ref(0);
onMounted(() => {
timerId.value = setInterval(() => {
count.value++;
}, 1000);
});
onBeforeUnmount(() => {
console.log('组件即将卸载,清除定时器');
if (timerId.value) clearInterval(timerId.value);
});
onUnmounted(() => {
console.log('组件已卸载,清理完成');
// 仅用于日志或最终清理(如全局状态)
});
</script>
<template>
<p>计时: {{ count }}</p>
</template>
onErrorCaptured
<script setup>
import { onErrorCaptured } from 'vue';
onErrorCaptured((error, instance, info) => {
console.error('捕获到错误:', error, info);
// 上报错误
return false; // 阻止向上冒泡
});
</script>
Composition API 与 Options API 生命周期对照表
| 阶段 | Options API (Vue 3) | Composition API |
|---|---|---|
| 创建前 | beforeCreate | setup() (替代) |
| 创建后 | created | setup() (替代) |
| 挂载前 | beforeMount | onBeforeMount() |
| 挂载后 | mounted | onMounted() |
| 更新前 | beforeUpdate | onBeforeUpdate() |
| 更新后 | updated | onUpdated() |
| 卸载前 | beforeUnmount | onBeforeUnmount() |
| 卸载后 | unmounted | onUnmounted() |
| 错误捕获 | errorCaptured | onErrorCaptured() |
| 激活(keep-alive) | activated | onActivated() |
| 失活(keep-alive) | deactivated | onDeactivated() |
⚠️ 注意:
setup()函数本身替代了beforeCreate和created钩子,因此在 Composition API 中无需再使用这两个钩子。
ASCII 图示:组件生命周期流程(Composition API)
+------------------+
| setup() |
| (替代 created) |
+--------+---------+
|
+--------v---------+
| onBeforeMount() |
+--------+---------+
|
+--------v---------+
| DOM 挂载 |
+--------+---------+
|
+--------v---------+
| onMounted() | <——— 发起请求、设定时器
+--------+---------+
|
+--------v---------+ +------------------+
| onBeforeUpdate() | <—— | 数据变化 (state) |
+--------+---------+ +------------------+
|
+--------v---------+
| DOM 更新 |
+--------+---------+
|
+--------v---------+
| onUpdated() | <—— 避免修改状态
+--------+---------+
|
+--------v---------+ +------------------+
| onBeforeUnmount()| <—— | 组件卸载 (v-if) |
+--------+---------+ +------------------+
|
+--------v---------+
| onUnmounted() | <—— 清理完成
+------------------+
箭头方向:生命周期执行顺序,[数据变化] → 触发更新周期,[组件卸载] → 触发销毁周期
💡 提示:
- 所有生命周期钩子在
<script setup>中必须在setup()同步执行期间注册(即顶层直接调用)。onMounted是最常用的钩子,用于处理 DOM 操作和异步数据获取。onBeforeUnmount是清理资源(如定时器、事件监听)的关键钩子,防止内存泄漏。- 使用
<keep-alive>时,onActivated和onDeactivated替代onMounted/onUnmounted控制组件激活与失活。
12. 响应式原理(进阶)
Vue 3 响应式核心:Proxy 与 Reflect
| 概念 | 描述 | 示例 |
|---|---|---|
| Proxy | Vue 3 使用 ES6 的 Proxy 对象来实现响应式数据绑定。它允许你拦截并自定义基本操作,如属性查找、赋值等。 | 见下方代码块 |
| Reflect | 与 Proxy 配合使用,提供了一组默认操作的 API,确保代理对象和原始对象行为一致。 | 见下方代码块 |
Proxy 示例:
const data = { count: 0 };
const proxyData = new Proxy(data, {
get(target, key) { console.log(`获取 ${key}`); return target[key]; },
set(target, key, value) { console.log(`设置 ${key} 为 ${value}`); target[key] = value; }
});
proxyData.count++; // 输出: 获取 count, 设置 count 为 1
Reflect 示例:
const data = { count: 0 };
const proxyData = new Proxy(data, {
set(target, key, value) {
Reflect.set(target, key, value); // 确保默认行为被执行
console.log(`更新了 ${key} 为 ${value}`);
}
});
proxyData.count = 1; // 输出: 更新了 count 为 1
ref 与 reactive 内部机制
| 函数 | 描述 | 示例 |
|---|---|---|
ref | 创建一个包含值的响应式引用对象。通过 .value 访问或修改其值。适用于基础类型或需要跨层级传递的数据。 | const count = ref(0); count.value++; |
reactive | 创建一个响应式的对象。直接修改对象属性即可触发视图更新。适合复杂数据结构。 | const state = reactive({ count: 0 }); state.count++; |
响应式丢失问题与解法(toRefs, toRef)
| 问题/解决方案 | 描述 | 示例 |
|---|---|---|
| 响应式丢失 | 当从 reactive 对象中解构时,失去响应性。例如,在函数参数或组件 props 中解构传入的对象。 | 见下方代码块 |
toRefs | 将 reactive 对象转换为普通对象,其中每个属性都是对应的 ref。适合在解构时保持响应性。 | 见下方代码块 |
toRef | 创建对源 reactive 对象中属性的响应式引用。如果属性不存在,则创建一个具有默认值的新 ref。 | 见下方代码块 |
响应式丢失:
const state = reactive({ count: 0 });
function updateCount({ count }) { count++; } // 失去响应性
updateCount(toRefs(state)); // 保持响应性
toRefs:
<script setup>
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0 });
const { count } = toRefs(state);
count.value++;
</script>
toRef:
<script setup>
import { reactive, toRef } from 'vue';
const state = reactive({});
const count = toRef(state, 'count', 0); // 默认值为 0
count.value++;
</script>
ASCII 图示:ref vs reactive 响应式处理
ref:
+-----------------+
| ref(count=0) |
| +-------------+ |
| | .value | |
| | (可变) | |
| +-------------+ |
+-----------------+
reactive:
+------------------+
| reactive(state) |
| +--------------+ |
| | count: 0 | |
| | (直接访问) | |
| +--------------+ |
+------------------+
toRefs/toRef:
+------------------+ +-----------------+
| reactive(state) |--toRefs--> | toRefs(state) |
| +--------------+ | | +-------------+ |
| | count: 0 | | | | count: ref | |
| | | | | | (保持响应) | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
箭头方向:转换流程
[ref]→ 单一值的响应式引用,需通过.value访问[reactive]→ 对象级别的响应式处理,直接访问属性[toRefs/toRef]→ 解决响应式丢失问题,保持数据流中的响应性
💡 提示:
ref和reactive是 Vue 3 实现响应式数据的核心方法,选择合适的方法取决于你的具体需求。toRefs和toRef提供了解决方案以避免响应式丢失的问题,尤其是在需要解构或作为 prop 传递数据的情况下。正确应用这些工具可以确保应用程序的状态管理更加流畅且不易出错。
第五部分:高级特性
13. 自定义 Hook(可组合函数)
封装可复用逻辑
| 概念 | 描述 | 示例 |
|---|---|---|
| 自定义 Hook | 在 Vue 3 中,通过组合式 API 创建的函数,用于封装和复用有状态的逻辑。这些函数通常以 use 开头命名。 | 见下方代码块 |
useFetch | 封装网络请求逻辑,返回响应数据、加载状态和错误信息。 | 见下方代码块 |
useLocalStorage | 封装本地存储逻辑,使数据在页面刷新后仍然保留。 | 见下方代码块 |
自定义 Hook 示例
useMouse.js:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.clientX;
y.value = e.clientY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
useFetch.js:
// useFetch.js
import { ref } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
fetch(url)
.then(res => res.json())
.then(json => data.value = json)
.catch(err => error.value = err)
.finally(() => loading.value = false);
return { data, error, loading };
}
useLocalStorage.js:
// useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key);
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
});
return value;
}
使用示例
使用 useMouse:
<script setup>
import { useMouse } from './useMouse';
const { x, y } = useMouse();
</script>
<template>
<div>鼠标位置: {{ x }}, {{ y }}</div>
</template>
使用 useFetch:
<script setup>
import { useFetch } from './useFetch';
const { data, error, loading } = useFetch('/api/data');
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<div v-else>数据: {{ data }}</div>
</template>
使用 useLocalStorage:
<script setup>
import { useLocalStorage } from './useLocalStorage';
const theme = useLocalStorage('theme', 'light');
</script>
<template>
<div :class="theme">当前主题: {{ theme }}</div>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">切换主题</button>
</template>
ASCII 图示:自定义 Hook 结构
+---------------------+
| useMouse() |
| |
| +--------------+ |
| | x: ref(0) | |
| +--------------+ |
| | y: ref(0) | |
| +--------------+ |
| | onMounted | |
| | onUnmounted | |
| +--------------+ |
| | return {x,y} | |
| +--------------+ |
+----------+----------+
|
v
+---------------------+
| useFetch(url) |
| |
| +--------------+ |
| | data:ref(null)| |
| +--------------+ |
| | error:ref(null)||
| +--------------+ |
| |loading:ref(true)||
| +--------------+ |
| | fetch() | |
| +--------------+ |
| |return {data, |
| | error, loading}|
| +--------------+ |
+----------+----------+
|
v
+---------------------+
| useLocalStorage |
| (key, initialValue) |
| |
| +--------------+ |
| |value:ref(init)| |
| +--------------+ |
| | watch(value) | |
| | localStorage | |
| +--------------+ |
| | return value | |
| +--------------+ |
+---------------------+
箭头方向:函数调用流程
[useMouse]→ 返回鼠标坐标 ref[useFetch]→ 返回数据、错误和加载状态 ref[useLocalStorage]→ 返回持久化 ref,自动同步到 localStorage
💡 提示:
- 自定义 Hook 是实现逻辑复用的强大工具,它们可以包含响应式数据、生命周期钩子和其他组合式 API。
- 确保每个 Hook 都是独立的,不依赖于特定组件的状态,以便在多个组件间复用。
- 使用
ref和reactive来管理状态,并利用watch或生命周期钩子来处理副作用。- 命名约定:所有自定义 Hook 应以
use开头,便于识别和遵循社区规范。
14. Teleport
实现模态框、弹窗等脱离 DOM 结构渲染
| 概念 | 描述 | 示例 |
|---|---|---|
| Teleport | Teleport 是 Vue 3 提供的一个内置组件,允许你将模板的一部分”传送”到 DOM 中的另一个位置。这对于模态框、弹窗、通知等需要脱离当前组件层级但逻辑上属于该组件的 UI 元素非常有用。 | 见下方代码块 |
to 属性 | Teleport 的 to 属性指定目标元素的选择器字符串(如 "body"、"#app"、.container)。被包裹的内容将被渲染到该目标元素内部。 | 见下方代码块 |
基本示例
<template>
<div class="component">
<h2>组件内容</h2>
<!-- 使用 Teleport 将模态框传送到 body 下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>这是一个模态框</p>
<button @click="hideModal">关闭</button>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showModal = ref(false);
const hideModal = () => {
showModal.value = false;
};
// 演示如何打开模态框
const openModal = () => {
showModal.value = true;
};
// 在组件挂载后添加按钮用于测试
import { onMounted } from 'vue';
onMounted(() => {
const btn = document.createElement('button');
btn.textContent = '打开模态框';
btn.onclick = openModal;
document.body.appendChild(btn);
});
</script>
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 1px solid #ccc;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
to 属性指定目标
<!-- 传送到 id 为 modal-root 的元素 -->
<Teleport to="#modal-root">
<div class="modal">...</div>
</Teleport>
<!-- 传送到 body 元素 -->
<Teleport to="body">
<div class="notification">新消息到达!</div>
</Teleport>
<!-- 条件传送 -->
<Teleport :to="teleportTarget">
<div>动态目标</div>
</Teleport>
<script setup>
import { ref } from 'vue';
const teleportTarget = ref('body'); // 可动态改变
</script>
ASCII 图示:Teleport 工作原理
原始 DOM 结构:
+-----------------------+
| <div id="app"> |
| +---------------+ |
| | 组件内容 | |
| | | |
| | <Teleport | |
| | to="body"> | |
| | <div class= | |
| | "modal"> | |
| | </div> | |
| | </Teleport> | |
| +---------------+ |
| </div> |
+-----------------------+
Teleport 后的实际 DOM 结构:
+-----------------------+ +-----------------------+
| <div id="app"> | | <body> |
| +---------------+ | | ... |
| | 组件内容 | | | +-------------+ |
| | | | | | <div class= | |
| | | | | | "modal"> | |
| | | | | | </div> | |
| | | | | +-------------+ |
| +---------------+ | | </body> |
| </div> | +-----------------------+
+-----------------------+
箭头方向:[Teleport 内容] → 从组件内部 → 被移动到 to 指定的目标位置(如 body)
⚠️ 注意:尽管 DOM 位置改变,但事件监听和响应式系统仍正常工作。
💡 提示:
- Teleport 不会改变组件的逻辑行为,事件和响应式数据依然正常工作。
- 常见用途:模态框(Modal)、弹出菜单(Dropdown)、工具提示(Tooltip)、全局通知(Notification)。
to目标元素必须在 Teleport 渲染时存在,否则内容不会被渲染。- 可以嵌套多个 Teleport,但应避免过度使用以免造成 DOM 结构混乱。
- 样式作用域:
<style scoped>不会影响 Teleport 外部的内容,需使用全局样式或:deep()穿透。
15. Suspense(实验性)
异步组件加载状态管理
| 概念 | 描述 | 示例 |
|---|---|---|
| Suspense | Suspense 是 Vue 3 中的一个实验性特性,它允许你为异步依赖(如异步组件或带有异步操作的组件)提供一个备用内容(fallback content),直到这些依赖项被解析为止。这使得你可以更优雅地处理异步数据加载的状态。 | 见下方代码块 |
#default | 在 <Suspense> 组件内,#default 插槽用于定义当所有异步依赖都被成功解析后显示的内容。通常这里放置的是主要的异步组件或者需要等待异步操作完成后才显示的部分。 | 见下方代码块 |
#fallback | 在 <Suspense> 组件内,#fallback 插槽用于定义在异步依赖未完成时显示的备用内容。这对于提升用户体验非常有用,可以告知用户内容正在加载中。 | 见下方代码块 |
基本示例
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
new Promise((resolve) => {
setTimeout(() => {
resolve({
template: '<div>异步组件加载完成!</div>'
});
}, 2000);
})
);
</script>
ASCII 图示:Suspense 工作原理
初始状态:
+------------------------+
| <Suspense> |
| +------------------+ |
| | #fallback | |
| | 加载中… | |
| +------------------+ |
| |
| (异步组件仍在加载) |
+------------------------+
最终状态(异步组件加载完毕):
+------------------------+
| <Suspense> |
| +------------------+ |
| | #default | |
| | 异步组件加载完成! | |
| +------------------+ |
| |
| (异步组件已加载) |
+------------------------+
箭头方向:从加载中到加载完成 [初始状态] → 等待异步组件加载 → [最终状态]
⚠️ 注意:实际渲染效果取决于异步组件的加载时间。
💡 提示:
- Suspense 需要 Vue 版本支持,并且目前是实验性功能,使用时请参考最新的 Vue 官方文档。
- 异步组件可以通过
defineAsyncComponent函数创建,该函数接受一个返回 Promise 的工厂函数作为参数。#default和#fallback插槽的组合提供了清晰的结构化方式来处理异步数据加载的不同状态。- 可以根据需求调整
#fallback内容,比如加入进度条、动画等增强用户体验的设计。
16. Fragments
支持多根节点组件
| 概念 | 描述 | 示例 |
|---|---|---|
| Fragments | Vue 3 引入了对多根节点组件的支持,这意味着一个组件的模板可以包含多个顶层元素,而不再强制要求必须有一个单一的根元素。 | 见下方代码块 |
基本示例
<template>
<header>
<h1>网站标题</h1>
</header>
<main>
<p>这是主要内容。</p>
</main>
<footer>
<p>版权信息</p>
</footer>
</template>
<script setup>
// 无需额外逻辑,组件可直接拥有多个根节点
</script>
使用示例
布局组件:
<!-- Layout.vue -->
<template>
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</template>
列表项组件:
<!-- ListItem.vue -->
<template>
<li class="list-item">
{{ item.text }}
</li>
<li class="list-item">
{{ item.description }}
</li>
</template>
<script setup>
const props = defineProps(['item']);
</script>
表格行组件:
<!-- TableRow.vue -->
<template>
<tr>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
<tr v-if="user.details">
<td colspan="2">{{ user.details }}</td>
</tr>
</template>
<script setup>
const props = defineProps(['user']);
</script>
ASCII 图示:Fragments 工作原理
Vue 2(单根限制):
+-----------------------+
| <div> |
| +---------------+ |
| | <header> | |
| +---------------+ |
| +---------------+ |
| | <main> | |
| +---------------+ |
| +---------------+ |
| | <footer> | |
| +---------------+ |
| </div> |
+-----------------------+
Vue 3(Fragments,多根节点):
+-----------------------+ +-----------------------+ +-----------------------+
| <header> | | <main> | | <footer> |
| <h1>标题</h1> | | <p>内容</p> | | <p>版权</p> |
| </header> | | </main> | | </footer> |
+-----------------------+ +-----------------------+ +-----------------------+
箭头方向:从单根到多根 [Vue 2] → 必须使用单一根元素包裹 → [Vue 3] → 可直接使用多个根元素
⚠️ 注意:实际渲染效果更加灵活,减少了不必要的包装元素。
💡 提示:
- Fragments 提升了组件设计的灵活性,特别是在构建布局和语义化 HTML 结构时。
- 多根节点组件在使用
v-model或某些指令时需要注意作用域和应用范围。- 当使用
<script setup>时,多根节点的行为与普通组件一致,无需额外配置。- 尽管允许多根节点,但在某些情况下仍需考虑 CSS 样式的影响,确保布局正确。
17. 异步组件
使用 defineAsyncComponent 管理异步组件
| 功能 | 描述 | 示例 |
|---|---|---|
| 基本用法 | 使用 defineAsyncComponent 来动态加载组件。当组件需要被渲染时,才会去加载它。这有助于优化首屏加载时间,特别是对于较大的应用。 | 见下方代码块 |
| 加载状态处理 | 可以通过提供一个对象作为参数,包含 loader 函数和 loadingComponent 组件来展示加载中的状态。 | 见下方代码块 |
| 错误处理 | 提供 errorComponent 属性用于显示加载失败时的界面,并可以通过 timeout 设置超时时间(毫秒),超过该时间将触发错误组件。 | 见下方代码块 |
| 延迟加载 | 使用 delay 参数指定延迟加载的时间(毫秒)。在延迟时间内,会首先显示 loadingComponent,直到达到延迟时间或组件加载完成。 | 见下方代码块 |
基本用法
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyAsyncComponent.vue')
);
加载状态处理
const AsyncComp = defineAsyncComponent({
loader: () => import('./components/MyAsyncComponent.vue'),
loadingComponent: LoadingComponent,
});
错误处理
const AsyncComp = defineAsyncComponent({
loader: () => import('./components/MyAsyncComponent.vue'),
errorComponent: ErrorComponent,
timeout: 3000, // 超时时间为3秒
});
延迟加载
const AsyncComp = defineAsyncComponent({
loader: () => import('./components/MyAsyncComponent.vue'),
delay: 2000, // 延迟加载2秒
loadingComponent: LoadingComponent,
});
ASCII 图示:异步组件生命周期
+---------------------+ +-----------------------+ +----------------------+
| 开始请求组件 | ----> | 正在加载中 | -----> | 加载成功/失败 |
| | | (显示Loading组件) | | |
| | | | | 成功:显示目标组件 |
| | | 超时或加载失败: | | 失败:显示Error组件 |
+---------------------+ +-----------------------+ +----------------------+
步骤说明:
- 请求开始加载异步组件。
- 如果设置了
loadingComponent,则显示。 - 根据加载结果,决定下一步操作:
- 加载成功:显示目标组件。
- 加载失败或超时:显示错误组件。
- 支持配置延迟、超时等参数以增强灵活性。
💡 提示:
- 使用异步组件可以帮助减少初始包大小,改善页面加载性能。
- 在设计时考虑用户等待体验,合理设置加载、错误界面以及延迟和超时参数。
- 当使用
defineAsyncComponent时,确保正确处理各种状态(加载、错误、延迟等),以便提供流畅的用户体验。
第六部分:状态管理
18. Pinia(官方推荐)
为什么使用 Pinia?
| 特性 | 描述 |
|---|---|
| ✅ 更简洁的 API | 相比 Vuex,Pinia 的 API 更加直观和简洁,学习成本更低。 |
| ✅ TypeScript 原生支持 | 完全用 TypeScript 编写,提供一流的类型推断支持。 |
| ✅ 模块化设计 | 不需要嵌套模块,每个 store 都是独立的,天然支持模块化。 |
| ✅ 无 Mutation 概念 | 直接在 actions 中修改 state,减少模板代码。 |
| ✅ DevTools 集成 | 自动集成 Vue DevTools,支持时间旅行调试。 |
| ✅ 轻量 | 体积小,性能高。 |
| ✅ 支持服务端渲染 (SSR) | 可在 Nuxt 3 等框架中使用。 |
安装与配置
| 步骤 | 命令 / 代码 |
|---|---|
| 安装 Pinia | npm install pinia 或 yarn add pinia |
| 创建并注册 Pinia 实例 | 见下方代码块 |
// main.js 或 main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
定义 Store:defineStore
defineStore 创建一个 store。第一个参数是唯一 ID,第二个参数是选项对象。
State、Getters、Actions
| 概念 | 描述 | 示例 |
|---|---|---|
| State | 存储应用状态。通过 store.$state 或直接访问属性读写。 | const store = useCounterStore()console.log(store.count) // 获取store.count = 10 // 设置 |
| Getters | 类似于计算属性,用于派生状态。支持缓存。 | console.log(store.double) // 20console.log(store.triple()(2)) // 60 |
| Actions | 用于定义业务逻辑,可包含同步或异步操作。是修改 state 的推荐方式。 | store.increment() // 同步await store.fetchData() // 异步 |
在组件中使用 Store
| 场景 | 示例 |
|---|---|
| 读取 State 和 Getters | 见下方代码块 |
| 调用 Actions | 见下方代码块 |
| 解构 State(保持响应性) | 见下方代码块 |
读取 State 和 Getters:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.double }}</p>
</div>
</template>
调用 Actions:
<template>
<button @click="counter.increment()">+1</button>
<button @click="counter.fetchData()">加载数据</button>
</template>
解构 State(保持响应性):
<script setup>
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// 使用 storeToRefs 包装,保持解构后的响应性
const { count, double } = storeToRefs(counter)
</script>
模块化与持久化(pinia-plugin-persistedstate)
| 功能 | 说明 | 示例 |
|---|---|---|
| 模块化 | 每个 defineStore 即为一个模块,无需额外配置。 | export const useUserStore = defineStore('user', { ... }) |
| 持久化插件安装 | 将状态持久化到 localStorage 或 sessionStorage。 | npm install pinia-plugin-persistedstate |
| 启用持久化 | 在 store 中使用 persist: true。 | 见下方代码块 |
| 自定义持久化配置 | 可指定存储位置、键名、路径等。 | 见下方代码块 |
启用持久化:
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
persist: true // 默认使用 localStorage
})
自定义持久化配置:
persist: {
key: 'my-counter',
storage: sessionStorage,
paths: ['count'] // 仅持久化 count 字段
}
在 Pinia 实例中使用插件:
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
ASCII 图示:Pinia 架构与数据流
+-------------------+
| Component |
| (读取/调用) |
+--------+----------+
|
| 使用
v
+--------+----------+
| Store |
| +---------------+ |
| | state | |<------------------+
| | (数据源) | | |
| +---------------+ | |
| | getters | | 计算 |
| | (派生状态) | | |
| +---------------+ | |
| | actions | | 修改 |
| | (业务逻辑) | |-------------------+
| +---------------+ |
+--------+----------+
|
| 持久化
v
+--------+----------+
| 浏览器存储 |
| (localStorage / |
| sessionStorage) |
+-------------------+
箭头方向:
- 组件 → Store:读取 state/getters,调用 actions
- Actions → State:修改状态
- State → 浏览器存储:通过持久化插件自动同步
💡 提示:
- 推荐使用
storeToRefs()解构 state,避免失去响应性。- actions 是修改 state 的唯一推荐方式(尽管可直接修改,但不推荐)。
- 持久化插件
pinia-plugin-persistedstate非常实用,尤其适合用户偏好、登录状态等场景。- 每个 store 应关注单一职责,便于维护和测试。
第七部分:路由管理
19. Vue Router 4
安装与配置
| 步骤 | 说明 | 示例 |
|---|---|---|
| 安装 | 使用 npm 或 yarn 安装 vue-router。 | npm install vue-router@4 或 yarn add vue-router@4 |
| 创建路由实例 | 创建 router/index.js,定义路由并创建 router 实例。 | 见下方代码块 |
| 挂载到应用 | 在 main.js 中引入并使用 router。 | 见下方代码块 |
创建路由实例:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{ path: '/', component: Home }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
挂载到应用:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
路由模式:hash vs history
| 模式 | 描述 | 示例 URL | 优点 | 缺点 |
|---|---|---|---|---|
| Hash | 使用 URL 的 hash (#) 来模拟一个完整的 URL,不会向服务器发送请求。 | http://localhost:3000/#/about | 兼容性好,无需服务器配置 | URL 不够美观,SEO 不友好 |
| History | 利用 HTML5 History API (pushState, replaceState),URL 看起来更”正常”。 | http://localhost:3000/about | 更友好的 URL,利于 SEO | 需要服务器支持,否则刷新会 404 |
配置方式:
// 使用 hash 模式
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 使用 history 模式(推荐)
const router = createRouter({
history: createWebHistory(),
routes
})
基本路由配置:routes
| 概念 | 示例 |
|---|---|
| 基础路由 | const routes = [ { path: '/', component: Home }, { path: '/about', component: About }] |
| 命名路由 | { path: '/user/:id', name: 'user', component: User } |
| 多视图(命名视图) | { path: '/dashboard', components: { default: Dashboard, sidebar: Sidebar } } |
路由跳转:<router-link> 和 router.push()
| 方式 | 说明 |
|---|---|
<router-link> | 声明式导航,生成可点击的链接。 |
router.push() | 编程式导航,用于逻辑中跳转。 |
动态路由、嵌套路由
| 类型 | 说明 |
|---|---|
| 动态路由 | 匹配带参数的路由。 |
| 嵌套路由 | 子路由通过 children 配置,配合 <router-view> 嵌套渲染。 |
路由守卫
| 类型 | 描述 | 示例 |
|---|---|---|
| 全局前置守卫 | 在路由切换前执行,常用于权限校验。 | 见下方代码块 |
| 路由独享守卫 | 在特定路由上定义的守卫。 | 见下方代码块 |
| 组件内守卫 | 在组件内部定义的守卫。 | 见下方代码块 |
全局前置守卫:
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
路由独享守卫:
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
if (!isAdmin()) return false // 阻止导航
}
}
组件内守卫:
<script setup>
// 进入组件前
const beforeRouteEnter = (to, from, next) => {
// 注意:不能访问 `this`
next(vm => { /* 可访问组件实例 */ })
}
// 路由更新时(如参数变化)
const beforeRouteUpdate = (to, from) => {
// 可访问 `this`
}
// 离开组件前
const beforeRouteLeave = (to, from) => {
if (!confirm('确定离开?')) return false
}
defineOptions({
beforeRouteEnter,
beforeRouteUpdate,
beforeRouteLeave
})
</script>
路由元信息(meta)
用于权限、标题、面包屑等额外信息携带。
路由懒加载
| 方式 | 说明 | 示例 |
|---|---|---|
| 动态导入 | 将组件按需加载,减少首屏体积。 | component: () => import('../views/About.vue') |
| 命名 chunk | Webpack 会将懒加载的组件打包为独立文件。 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') |
ASCII 图示:Vue Router 架构与导航流程
+-------------------+
| 用户操作 |
| (点击链接或跳转) |
+--------+----------+
|
v
+--------+----------+
| <router-link> |
| 或 router.push() |
+--------+----------+
|
v
+--------+----------+
| 全局守卫 |
| (beforeEach) |
+--------+----------+
|
v
+--------+----------+
| 路由独享守卫 |
| (beforeEnter) |
+--------+----------+
|
v
+--------+----------+
| 组件内守卫 |
| (beforeRouteLeave/Enter) |
+--------+----------+
|
v
+--------+----------+
| 匹配路由 |
| 渲染组件 |
| <router-view> |
+-------------------+
箭头方向:导航流程 [用户操作] → [触发跳转] → [守卫检查] → [匹配并渲染]
💡 提示:
- 推荐使用 history 模式,但需确保服务器配置正确(如 Nginx 重定向到 index.html)。
- 路由懒加载是性能优化的关键手段,应广泛使用。
meta字段是传递路由相关信息的绝佳方式,尤其适合权限和页面标题管理。- 守卫函数中必须调用
next()或返回true/false否则导航会被阻塞。
第八部分:工具与工程化
20. TypeScript 支持
Vue 3 + TS 项目配置
| 步骤 | 说明 | 示例 |
|---|---|---|
| 创建项目 | 使用 Vite 或 Vue CLI 创建支持 TypeScript 的项目。 | npm create vue@latest(在提示中选择 TypeScript) |
| 关键依赖 | 确保项目包含 TypeScript 和 Vue 类型支持。 | 见下方代码块 |
tsconfig.json | 配置 TypeScript 编译选项。 | 见下方代码块 |
| 类型检查命令 | 推荐在 CI 或构建前运行类型检查。 | "type-check": "vue-tsc --noEmit" |
关键依赖:
// package.json
{
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^18.0.0",
"vue-tsc": "^1.8.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
defineComponent 类型定义
| 概念 | 描述 | 示例 |
|---|---|---|
defineComponent | Vue 3 中用于定义组件的函数,提供完整的类型推断支持,尤其在使用 <script setup> 外部定义组件时。 | 见下方代码块 |
配合 <script setup> | 在 <script setup> 中可省略 defineComponent,但仍需类型支持。 | 见下方代码块 |
defineComponent:
<!-- MyComponent.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
msg: { type: String, required: true }
},
setup(props) {
// props.msg 类型自动推断为 string
return () => <div>{props.msg}</div>
}
})
</script>
配合 <script setup>:
<script setup lang="ts">
// 类型直接在 script 中使用
interface User {
id: number
name: string
}
const user: User = { id: 1, name: 'Alice' }
</script>
ref 类型推断与断言
| 场景 | 说明 |
|---|---|
| 自动类型推断 | ref 会根据初始值自动推断类型。 |
| 显式类型断言 | 当初始值为 null 或 undefined 时,需手动指定类型。 |
访问 .value | ref 的值通过 .value 访问,类型安全。 |
Props 类型安全
| 方式 | 说明 |
|---|---|
使用 defineProps(推荐) | 在 <script setup> 中使用泛型或对象定义 props 类型。 |
| 运行时声明 + 类型校验 | 使用 defineProps 与运行时类型结合。 |
复杂类型使用 PropType | 当类型无法自动推断时(如函数、对象),使用 PropType。 |
ASCII 图示:Vue + TypeScript 类型流
+-----------------------+
| defineProps<T>() |
| (声明 Props 类型) |
+----------+------------+
|
v
+----------+------------+
| <script setup> |
| const props = ... |
| props.xxx 类型安全 |
+----------+------------+
|
v
+----------+------------+
| ref<T>() / reactive<T> |
| (状态类型安全) |
+----------+------------+
|
v
+----------+------------+
| 模板编译时检查 |
| (Volar / Vue-tsc) |
+-----------------------+
箭头方向:类型信息流动 [Props定义] → [组件逻辑] → [响应式数据] → [模板使用]
💡 提示:
- 使用
<script setup lang="ts">是当前 Vue + TS 的最佳实践。withDefaults用于设置默认值并保留类型推断。- 推荐使用 Volar 替代 Vetur,提供更好的 TS 支持。
- 对于
ref操作 DOM 元素,务必使用泛型断言(如ref<HTMLDivElement>)。- 在大型项目中,建议将接口(interface)抽离到单独的
.ts文件中复用。
21. Vite 构建工具
快速启动、热更新原理
| 概念 | 描述 |
|---|---|
| 快速启动 | Vite 利用现代浏览器原生支持 ES 模块(ESM),在开发时无需打包,直接按需加载模块,实现秒级启动。 |
| 热更新(HMR)原理 | 当文件修改时,Vite 通过 WebSocket 通知浏览器,仅替换修改的模块,无需刷新整个页面,保持应用状态。 |
# 创建项目
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
# 启动速度极快(通常 < 1s)
ASCII 图示:Vite 开发服务器工作原理
+-------------------+ +-------------------+
| 浏览器请求 | --> | Vite Dev Server |
| /src/main.js | | |
+-------------------+ +-------------------+
|
v
+---------------------------+
| 按需转换(On-demand) |
| - .vue → JS |
| - TypeScript → JS |
| - CSS / SCSS |
| - 静态资源 |
+---------------------------+
|
v
+---------------------------+
| 返回 ESM 模块 |
| 浏览器原生加载 |
+---------------------------+
HMR 流程:[文件修改] → [Server 监听] → [WebSocket 通知] → [浏览器替换模块] → [页面局部更新]
插件系统
| 插件类型 | 说明 | 示例 |
|---|---|---|
| 官方插件 | 如 @vitejs/plugin-vue 支持 .vue 文件。 | 见下方代码块 |
| 社区插件 | 如 vite-plugin-svg-icons、unplugin-auto-import 等。 | npm install unplugin-auto-import -D |
| 自定义插件 | 使用 Rollup 插件 API 创建自定义逻辑。 | 见下方代码块 |
官方插件:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()]
})
自定义插件:
// vite.config.js
export default defineConfig({
plugins: [
{
name: 'log-plugin',
buildStart() {
console.log('Build started!')
}
}
]
})
环境变量与模式
| 概念 | 描述 | 示例 |
|---|---|---|
| 环境变量文件 | Vite 支持 .env、.env.local、.env.[mode] 文件。 | .env.development: VITE_API_URL=http://localhost:3000.env.production: VITE_API_URL=https://api.example.com |
| 变量前缀 | 只有以 VITE_ 开头的变量才会暴露给客户端代码。 | console.log(import.meta.env.VITE_API_URL) ✅console.log(import.meta.env.API_KEY) ❌ undefined |
| 模式(Mode) | 通过 --mode 指定环境模式。 | "build:staging": "vite build --mode staging" |
构建生产包
| 步骤 | 说明 | 示例 |
|---|---|---|
| 构建命令 | 使用 vite build 生成生产环境资源,默认输出到 dist/ 目录。 | npm run build |
| 构建配置 | 自定义输出目录、压缩、代码分割等。 | 见下方代码块 |
| 预览生产包 | 使用 vite preview 预览构建结果。 | npm run build && npm run preview |
构建配置:
// vite.config.js
export default defineConfig({
build: {
outDir: 'dist', // 输出目录
minify: 'terser', // 压缩方式
sourcemap: false, // 是否生成 source map
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/assets/[name]-[hash].[ext]'
}
}
}
})
ASCII 图示:Vite 构建流程
开发模式: [浏览器] ←-- ESM 加载 --→ [Vite Dev Server] ←-- 监听文件 --→ [文件系统]
生产构建: [源码] → [Rollup 打包] → [压缩/混淆] → [静态资源] → [dist/ 目录]
└─ 基于 rollup.config.js(Vite 内置)
💡 提示:
- Vite 开发服务器不打包,直接服务源码,因此启动极快。
- 所有环境变量通过
import.meta.env访问。- 插件执行顺序:
pre→normal→post。- 生产构建默认启用 Tree Shaking 和代码分割。
- 推荐使用
vite build && vite preview验证构建结果后再部署。
22. 单文件组件(SFC)
<template>, <script setup>, <style> 结构
| 标签 | 描述 |
|---|---|
<template> | 定义组件的 HTML 模板结构。 |
<script setup> | 使用组合式 API 的语法糖,代码在组件实例创建前执行,无需 setup() 函数。 |
<style scoped> | 定义组件的样式,支持作用域控制。 |
<template>
<div class="greeting">
<h1>{{ msg }}</h1>
<button @click="onClick">点击我</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('Hello Vue')
function onClick() {
alert(msg.value)
}
</script>
<style scoped>
.greeting {
text-align: center;
color: #333;
}
</style>
<script setup> 语法糖(推荐)
| 特性 | 描述 | 示例 |
|---|---|---|
| 自动暴露变量和函数 | 所有在 <script setup> 中声明的顶层变量、函数都会自动暴露给模板使用。 | const name = 'Vue' + <p>{{ name }}</p> |
| 导入组件自动注册 | 导入的 .vue 文件可直接在模板中使用,无需注册。 | import Child from './Child.vue' + <Child /> |
顶层 await 支持 | 可直接使用 await,组件会自动变为异步。 | const data = await fetch('/api/data').then(res => res.json()) |
defineProps, defineEmits, defineExpose
| 函数 | 描述 | 示例 |
|---|---|---|
defineProps | 声明组件接收的 props,支持类型推断。 | const props = defineProps<{ title?: string; count: number }>() |
defineEmits | 声明组件触发的事件,支持类型检查。 | const emit = defineEmits<{ (e: 'update', id: number): void }>() |
defineExpose | 指定组件通过模板引用(ref)暴露的属性和方法。 | 见下方代码块 |
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
// 暴露给父组件使用
defineExpose({
count,
increment
})
</script>
CSS 作用域:scoped
| 概念 | 描述 |
|---|---|
scoped | 为 <style> 添加 scoped 属性后,样式仅作用于当前组件,避免全局污染。Vite/Vue 会自动为元素添加唯一属性选择器。编译后类似:.greeting[data-v-f3sf2] |
| 深度选择器 | 使用 :deep() 包裹子组件样式以穿透作用域。 |
| 全局样式 | 使用 :global() 定义全局样式。 |
CSS 预处理器支持
| 预处理器 | 安装命令 | lang 属性 |
|---|---|---|
| Sass/SCSS | npm install -D sass | lang="scss" |
| Less | npm install -D less | lang="less" |
| Stylus | npm install -D stylus | lang="stylus" |
ASCII 图示:SFC 编译流程
+-----------------------+
| MyComponent.vue |
| +-------------------+ |
| | <template> | |
| | <div class="c"> | |
| | {{ msg }} | |
| | </div> | |
| +-------------------+ |
| | <script setup> | |
| | const msg = ... | |
| +-------------------+ |
| | <style scoped> | |
| | .c { color: red }| |
| +-------------------+ |
+-----------------------+
|
v
[Vue SFC Compiler]
|
v
+-----------------------+ +-----------------------+
| JavaScript Module | | CSS with Scoped |
| - Render Function | | .c[data-v-abc123] |
| - Setup Logic | | { color: red } |
+-----------------------+ +-----------------------+
最终注入到应用中,实现结构、逻辑、样式的封装。
💡 提示:
<script setup>是 Vue 3 推荐的组合式 API 写法,简洁高效。defineProps和defineEmits在<script setup>中是宏命令,无需导入。scoped是实现样式模块化的关键,避免意外样式覆盖。- 预处理器需安装对应依赖,Vite 会自动识别
lang属性。- 可同时存在多个
<style>标签,例如一个scoped,一个:global。
第九部分:性能优化与最佳实践
23. 性能优化
使用 v-memo(Vue 3.2+)
| 概念 | 描述 | 示例 |
|---|---|---|
| v-memo | 记忆化指令,仅当依赖项发生变化时才重新渲染该部分模板,避免不必要的虚拟 DOM diff。适用于大型静态列表或复杂渲染块。 | <div v-for="item in list" :key="item.id" v-memo="[item.name, item.status]">{{ item.name }} - {{ item.status }}</div> |
| 注意事项 | 依赖数组必须正确,否则可能导致错误渲染。不适用于动态内容频繁变化的场景。 | <!-- 错误:依赖不完整 --><div v-memo="[item.name]" v-for="item in list">{{ item.name }} {{ item.count }} <!-- count 变化不会触发更新 --></div> |
避免不必要的响应式(markRaw, shallowRef)
| 方法 | 描述 | 示例 |
|---|---|---|
| markRaw | 标记一个对象为”不可被代理”,Vue 不会将其转换为响应式。适用于第三方库实例、大型不可变数据等。 | import { markRaw } from 'vue'const rawObject = markRaw({ someProperty: 'value', chartInstance: new Chart() })state.cachedComponents = { comp: rawObject } |
| shallowRef | 创建一个浅层 ref:只有 .value 是响应式的,其内部属性不是。适合大型对象或性能敏感场景。 | import { shallowRef, triggerRef } from 'vue'const state = shallowRef({ list: veryLargeArray })state.value.list.push(newItem); triggerRef(state) |
列表优化:key 合理使用
| 原则 | 描述 | 示例 |
|---|---|---|
| 使用唯一且稳定 key | 避免使用 index 作为 key,尤其是在列表可能增删或排序时,会导致组件状态错乱或重渲染。 | <!-- 推荐:使用唯一 ID --><li v-for="user in users" :key="user.id">{{ user.name }}</li><!-- 不推荐:使用 index --><li v-for="(user, index) in users" :key="index"><!-- 删除第一个元素后,所有 key 都变了 --></li> |
| key 的作用 | 帮助 Vue 跟踪每个节点的身份,尽可能复用和重新排序现有元素,提高 diff 效率。 | 列表更新前: [A, B, C] keys: [1,2,3] → 列表更新后: [C, A, B] keys: [3,1,2],Vue 知道 A/B/C 仍存在,只需移动位置,无需重建。 |
组件懒加载
| 方式 | 描述 | 示例 |
|---|---|---|
| 路由懒加载 | 结合 Vue Router,按需加载路由组件。 | const routes = [{ path: '/dashboard', component: () => import('./views/Dashboard.vue') }] |
| 异步组件(非路由) | 使用 defineAsyncComponent 懒加载非路由组件。 | import { defineAsyncComponent } from 'vue'const AsyncModal = defineAsyncComponent(() => import('./components/HeavyModal.vue')) |
使用 keep-alive 缓存组件
| 特性 | 描述 | 示例 |
|---|---|---|
<keep-alive> | 包裹动态组件时,会缓存不活动的组件实例,避免重复渲染和状态丢失。 | <keep-alive><component :is="currentTab" /></keep-alive> |
| include / exclude | 控制哪些组件需要被缓存。支持字符串、正则、数组。 | <keep-alive include="Home,User"><component :is="view" /></keep-alive><keep-alive :include="['Home', 'User']">...</keep-alive> |
| 生命周期钩子 | 被缓存组件可使用 onActivated() 和 onDeactivated() | onActivated(() => { console.log('组件被激活') })onDeactivated(() => { console.log('组件被缓存') }) |
keep-alive 缓存机制:
普通组件切换:
[Component A] --销毁--> [Component B] --创建-->
使用 keep-alive:
[Component A] --缓存--> [Component B] --创建-->
↑_________________________↓ 切换回来时直接激活缓存实例
内部结构:
+---------------------+
| <keep-alive> |
| +---------------+ |
| | Cache Map | | → 存储组件实例 { name: instance }
| +---------------+ |
| +---------------+ |
| | Current View | | → 当前显示的组件
| +---------------+ |
+---------------------+
💡 提示:
v-memo是 Vue 3.2+ 的高级优化手段,适用于性能瓶颈场景。markRaw和shallowRef可显著减少响应式系统开销,尤其在处理大型数据或第三方对象时。key是列表渲染性能的基础,务必使用唯一 ID。- 懒加载 +
keep-alive是提升用户体验的黄金组合:首次加载慢但后续极快。- 注意
keep-alive会占用内存,避免缓存过多大型组件。
24. 开发规范
目录结构建议
| 目录 | 用途 |
|---|---|
src/ | 源码主目录 |
src/
├── assets/ # 静态资源(图片、字体等)
├── components/ # 通用组件
│ ├── ui/ # UI 库组件(按钮、卡片等)
│ └── layout/ # 布局组件
├── views/ # 页面级组件(路由组件)
├── composables/ # 组合式函数(useXxx)
├── utils/ # 工具函数
├── stores/ # 状态管理(Pinia)
├── router/ # 路由配置
├── styles/ # 全局样式、变量
├── types/ # TypeScript 类型定义
├── api/ # 接口请求模块
└── main.ts # 入口文件
组件命名规范
| 规则 | 说明 | 示例 |
|---|---|---|
| 多单词命名 | 避免使用原生 HTML 标签名,组件名应为多个单词,用连字符或大驼峰。 | ✅ UserCard.vue, BaseButton.vue❌ Button.vue, input.vue |
| 命名空间前缀 | 通用组件使用 Base, App, Ui 等前缀区分。 | BaseButton.vue:基础按钮AppHeader.vue:应用头部UiCard.vue:UI 卡片 |
| 文件扩展名 | .vue 文件使用 PascalCase(大驼峰),.ts/.js 文件使用 camelCase(小驼峰)。 | UserProfile.vue, BaseModal.vue / formatDate.ts, apiClient.ts |
| 模板中使用 | 在模板中建议使用 kebab-case(短横线分隔)以兼容 HTML。 | <template><user-profile /><base-button>提交</base-button></template> |
代码风格(ESLint + Prettier)
| 工具 | 说明 | 配置示例 |
|---|---|---|
| ESLint | JavaScript/TypeScript 代码质量检查工具,识别潜在错误和风格问题。 | npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev |
| Prettier | 代码格式化工具,统一缩进、引号、换行等。 | npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev |
.eslintrc.cjs:
// .eslintrc.cjs
module.exports = {
root: true,
env: { node: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
'no-console': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off'
}
};
.prettierrc:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}
VS Code 集成:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript", "typescript", "vue"]
}
提交规范(Commitlint)
| 概念 | 说明 | 示例 |
|---|---|---|
| 提交消息格式 | 采用 Conventional Commits 规范:<type>[optional scope]: <description> | feat(user): add login formfix: prevent crash on null valuedocs: update READMEchore: update deps |
| 常用 type | feat:新功能fix:bug 修复docs:文档更新style:格式调整refactor:重构perf:性能优化test:测试相关chore:构建或辅助工具变动 | - |
| 安装与配置 | 使用 Commitlint 和 Husky 强制校验提交信息。 | 见下方命令 |
| 提交示例 | 使用 git commit 时必须符合规范,否则被拒绝。 | git commit -m "feat: add dark mode toggle" ✅git commit -m "updated something" ❌ |
# 安装
npm install @commitlint/{config-conventional,cli} --save-dev
npm install husky --save-dev
# 创建配置
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# 启用 husky
npx husky install
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
开发规范协同流程:
+----------------+ +----------------+ +--------------------+
| 编码 | --> | Git 提交 | --> | CI/CD 流水线 |
| (ESLint + | | (Commitlint) | | (自动检查、测试) |
| Prettier) | | | | |
+----------------+ +----------------+ +--------------------+
| | |
v v v
[格式统一、无错误] [提交信息规范] [高质量、可追溯的发布]
💡 提示:
- 一致的目录结构提升项目可维护性,团队成员可快速定位文件。
- 组件命名使用多单词避免与 HTML 标签冲突,提升可读性。
- ESLint + Prettier + VS Code 配置可实现”保存即格式化”,减少代码审查负担。
- Commitlint 结合 Husky 可强制团队遵守提交规范,便于生成 CHANGELOG 和语义化版本发布。
- 推荐使用
commitlint-config-cz或commitizen简化规范提交。
第十部分:实战项目与生态
25. 实战项目建议
1. TodoList(基础)
目标:掌握组件化、响应式数据、事件处理、本地存储。
功能点:
- 添加、删除任务
- 标记完成状态
- 持久化到 localStorage
最小可用代码示例:
<!-- TodoList.vue -->
<script setup>
import { ref, onMounted } from 'vue'
const todos = ref([])
const newTodo = ref('')
// 从 localStorage 加载
onMounted(() => {
const saved = localStorage.getItem('todos')
if (saved) todos.value = JSON.parse(saved)
})
// 保存到 localStorage
const saveTodos = () => {
localStorage.setItem('todos', JSON.stringify(todos.value))
}
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
saveTodos()
}
}
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
saveTodos()
}
const toggleComplete = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
saveTodos()
}
</script>
<template>
<div class="todo-app">
<h1>我的待办</h1>
<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="添加新任务..." />
<button type="submit">添加</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }">
<span @click="toggleComplete(todo.id)">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">×</button>
</li>
</ul>
</div>
</template>
<style scoped>
.todo-app { max-width: 400px; margin: 0 auto; }
input, button { padding: 8px; margin: 5px; }
ul { list-style: none; padding: 0; }
li { display: flex; justify-content: space-between; border-bottom: 1px solid #eee; }
.completed span { text-decoration: line-through; color: #888; }
</style>
2. 博客系统(含路由、状态)
目标:掌握 Vue Router、Pinia 状态管理、异步数据加载。
功能点:
- 文章列表页
- 文章详情页
- 使用 Pinia 管理文章数据
最小可用代码示例:
// stores/blogStore.js (Pinia)
import { defineStore } from 'pinia'
export const useBlogStore = defineStore('blog', {
state: () => ({
posts: [
{ id: 1, title: '第一篇文章', content: '这是内容...' },
{ id: 2, title: '第二篇', content: '继续写...' }
]
}),
getters: {
postCount: (state) => state.posts.length
},
actions: {
async fetchPosts() {
// 模拟异步加载
await new Promise(r => setTimeout(r, 500))
// 实际项目中可从 API 获取
}
}
})
<!-- views/PostList.vue -->
<script setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useBlogStore } from '../stores/blogStore'
const store = useBlogStore()
const router = useRouter()
onMounted(async () => {
await store.fetchPosts()
})
</script>
<template>
<div>
<h1>博客文章</h1>
<ul>
<li v-for="post in store.posts" :key="post.id">
<router-link :to="`/post/${post.id}`">{{ post.title }}</router-link>
</li>
</ul>
</div>
</template>
<!-- views/PostDetail.vue -->
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useBlogStore } from '../stores/blogStore'
const route = useRoute()
const store = useBlogStore()
const post = computed(() => {
return store.posts.find(p => p.id === Number(route.params.id))
})
</script>
<template>
<div v-if="post">
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<router-link to="/">← 返回</router-link>
</div>
<div v-else>文章未找到</div>
</template>
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import PostList from '../views/PostList.vue'
import PostDetail from '../views/PostDetail.vue'
const routes = [
{ path: '/', component: PostList },
{ path: '/post/:id', component: PostDetail }
]
export const router = createRouter({
history: createWebHistory(),
routes
})
3. 后台管理系统(权限、表格、图表)
目标:掌握权限控制、复杂表单、第三方库集成(如 ECharts)、Axios 请求。
功能点:
- 登录页(模拟)
- 用户列表(表格)
- 权限判断(仅管理员可见)
最小可用代码示例:
<!-- components/UserTable.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const users = ref([])
onMounted(async () => {
try {
// 使用 JSONPlaceholder 模拟 API
const res = await axios.get('https://jsonplaceholder.typicode.com/users')
users.value = res.data.slice(0, 5).map(u => ({ id: u.id, name: u.name, email: u.email }))
} catch (err) {
console.error('加载用户失败', err)
}
})
</script>
<template>
<div class="user-table">
<h2>用户列表</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
<!-- views/AdminDashboard.vue -->
<script setup>
// 假设从 Pinia 或全局状态获取角色
const role = 'admin' // 模拟
</script>
<template>
<div v-if="role === 'admin'" class="admin-dashboard">
<h1>管理员面板</h1>
<UserTable />
</div>
<div v-else>您没有权限访问此页面。</div>
</template>
4. 移动端 H5 应用
目标:掌握移动端适配、触摸事件、离线能力。
功能点:
- 使用 meta viewport
- 手势滑动切换内容
- 支持 PWA(渐进式 Web 应用)
最小可用代码示例:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>移动应用</title>
<link rel="manifest" href="/manifest.json">
</head>
<body>
<div id="app"></div>
</body>
</html>
// public/manifest.json
{
"name": "My Mobile App",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#42b983"
}
<!-- views/MobileSwipe.vue -->
<script setup>
import { ref } from 'vue'
const currentIndex = ref(0)
const pages = ['首页', '发现', '我的']
const swipeLeft = () => {
if (currentIndex.value < pages.length - 1) currentIndex.value++
}
const swipeRight = () => {
if (currentIndex.value > 0) currentIndex.value--
}
</script>
<template>
<div class="mobile-app">
<div class="swipe-container" @swipeleft="swipeLeft" @swiperight="swipeRight">
<div class="page" v-for="(page, index) in pages" :key="index" v-show="index === currentIndex">
{{ page }}
</div>
</div>
<div class="dots">
<span v-for="(_, i) in pages" :key="i" :class="{ active: i === currentIndex }"></span>
</div>
</div>
</template>
<style scoped>
.mobile-app { height: 100vh; overflow: hidden; font-size: 18px; }
.swipe-container { height: 100%; position: relative; }
.page {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
}
.dots {
position: fixed;
bottom: 20px;
width: 100%;
text-align: center;
}
.dots span {
display: inline-block;
width: 8px;
height: 8px;
background: #ccc;
border-radius: 50%;
margin: 0 4px;
}
.dots span.active { background: #42b983; }
</style>
<!-- 注:真实项目中需引入 Hammer.js 或使用 touch 事件实现手势 -->
💡 提示:
- TodoList 是入门必做,巩固基础。
- 博客系统引入路由和状态管理,迈向中等复杂度。
- 后台系统集成真实请求和权限,贴近企业开发。
- 移动端 H5 关注用户体验和性能,可结合 Cordova 打包为 App。
- 所有项目建议使用 Vite + Vue 3 + TS + Pinia + Vue Router 技术栈。
26. 常用生态库
UI 框架
| 库名 | 说明 | 安装命令 | 最小可用示例 |
|---|---|---|---|
| Element Plus | 面向企业级应用的桌面端 UI 库,风格简洁。 | npm install element-plus | import ElementPlus from 'element-plus'import 'element-plus/dist/index.css'app.use(ElementPlus)<el-button type="primary">主要按钮</el-button><el-input v-model="input" placeholder="请输入" /> |
| Ant Design Vue | 企业级 UI 设计语言的 Vue 实现,功能丰富。 | npm install ant-design-vue | import Antd from 'ant-design-vue'import 'ant-design-vue/dist/reset.css'createApp(App).use(Antd).mount('#app')<a-button type="primary">Ant 按钮</a-button><a-input v-model:value="value" placeholder="输入" /> |
| Vuetify | 实现 Material Design 的 Vue UI 框架,移动端友好。 | npm install vuetify@^3.0 | import { createVuetify } from 'vuetify'import 'vuetify/styles'const vuetify = createVuetify({ components, directives })<v-btn color="primary">Vuetify 按钮</v-btn> |
图表库
| 库名 | 说明 | 安装命令 | 最小可用示例 |
|---|---|---|---|
| ECharts | 百度开源的强大图表库,支持丰富图表类型。 | npm install echarts | import * as echarts from 'echarts' 初始化图表并设置 option |
| Chart.js | 轻量级、易于上手的图表库,适合基础图表。 | npm install chart.js | import { Chart } from 'chart.js/auto' 在 canvas 上创建图表 |
状态管理
| 库名 | 说明 | 安装命令 | 最小可用示例 |
|---|---|---|---|
| Pinia | Vue 官方推荐的状态管理库,TypeScript 友好,API 简洁。 | npm install pinia | defineStore('counter', { state, getters, actions }),组件中使用 useCounterStore() |
请求库
| 库名 | 说明 | 安装命令 | 最小可用示例 |
|---|---|---|---|
| Axios | 基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js,功能强大。 | npm install axios | axios.get('/api/users'), axios.post('/api/users', { name: 'John' })创建实例: axios.create({ baseURL, timeout }) |
| Fetch API | 浏览器原生支持,无需安装,轻量但功能较基础。 | (无需安装) | fetch('/api/data').then(res => res.json()).then(console.log) |
工具库
| 库名 | 说明 | 安装命令 | 最小可用示例 |
|---|---|---|---|
| Lodash | JavaScript 工具函数库,提供 debounce, throttle, cloneDeep 等实用函数。 | npm install lodash | import { debounce, throttle, cloneDeep } from 'lodash-es'防抖: debounce(fn, 300)节流: throttle(fn, 100)深拷贝: cloneDeep(obj) |
| Day.js | 轻量级日期处理库,API 类似 Moment.js,但体积更小。 | npm install dayjs | import dayjs from 'dayjs'格式化: dayjs().format('YYYY-MM-DD HH:mm:ss')相对时间: dayjs().subtract(1, 'day').fromNow() |
生态库集成关系:
+-----------------+
| Vue App |
| +-------------+ |
| | UI Framework| | ← Element Plus, Ant Design Vue, Vuetify
| +-------------+ |
| | State | | ← Pinia
| | Management | |
| +-------------+ |
| | HTTP Client | | ← Axios / Fetch
| +-------------+ |
| | Charts | | ← ECharts / Chart.js
| +-------------+ |
| | Utils | | ← Lodash / Day.js
| +-------------+ |
+-----------------+
|
v
[用户界面与交互]
💡 提示:
- UI 框架:Element Plus 适合中后台,Vuetify 适合移动端。
- 图表:ECharts 功能全面,Chart.js 简单易用。
- 状态管理:Pinia 是 Vue 3 的首选,替代 Vuex。
- 请求库:Axios 功能多,适合复杂项目;Fetch 原生轻量,适合简单场景。
- 工具库:Lodash 提供函数式编程工具,Day.js 处理日期更高效。
附录:速查表(Cheat Sheet)
1. 基础语法
| 类别 | 语法 | 说明 |
|---|---|---|
| 插值 | {{ message }} | 文本插值,自动转义 HTML |
| 原始 HTML | <div v-html="htmlContent"></div> | 插入原始 HTML(注意 XSS 风险) |
| 属性绑定 | :id="dynamicId" 或 v-bind:id="dynamicId" | 动态绑定属性 |
| 事件绑定 | @click="handler" 或 v-on:click="handler" | 绑定事件 |
| 条件渲染 | v-if, v-else-if, v-else | 条件渲染元素 |
| 列表渲染 | v-for="item in items" | 遍历数组或对象 |
| 双向绑定 | v-model="text" | 表单输入与数据双向绑定 |
| 修饰符 | @click.stop, @submit.prevent, v-model.trim | 事件/表单修饰符 |
2. 响应式 API(Composition API)
| API | 用法 | 说明 |
|---|---|---|
| ref() | const count = ref(0) | 创建响应式基本类型,通过 .value 访问 |
| reactive() | const state = reactive({ name: 'Vue' }) | 创建响应式对象,直接访问属性 |
| computed() | const double = computed(() => count.value * 2) | 创建计算属性 |
| watch() | watch(count, (newVal, oldVal) => { ... }) | 监听响应式数据变化 |
| watchEffect() | watchEffect(() => console.log(count.value)) | 自动追踪依赖,立即执行 |
| toRefs() | const { name } = toRefs(props) | 将 reactive 对象转为 ref,解构后仍保持响应性 |
3. 生命周期钩子
| 钩子 | 用法 | 说明 |
|---|---|---|
| onMounted | onMounted(() => { ... }) | 组件挂载后执行 |
| onUnmounted | onUnmounted(() => { ... }) | 组件卸载后执行 |
| onUpdated | onUpdated(() => { ... }) | 组件更新后执行 |
| onBeforeMount | onBeforeMount(() => { ... }) | 挂载前 |
| onBeforeUpdate | onBeforeUpdate(() => { ... }) | 更新前 |
| onBeforeUnmount | onBeforeUnmount(() => { ... }) | 卸载前 |
⚠️ 在
<script setup>中直接使用,无需setup()函数。
4. 组件通信
| 方式 | 示例 | 说明 |
|---|---|---|
| Props | defineProps(['title', 'disabled']) 或 defineProps({ title: String, count: { type: Number, default: 0 } }) | 父组件 → 子组件传值 |
| Emits | const emit = defineEmits(['update', 'delete']); emit('update', newValue) | 子组件 → 父组件触发事件 |
| Slots | <slot></slot> / <slot name="header"></slot> | 插槽分发内容 |
| Provide / Inject | provide('theme', 'dark') / const theme = inject('theme', 'light') | 跨层级组件传值 |
5. 模板引用(Template Refs)
<script setup>
import { ref, onMounted } from 'vue'
// DOM 引用
const inputRef = ref(null)
// 组件引用(需 defineExpose)
const childRef = ref(null)
onMounted(() => {
inputRef.value.focus() // 访问原生 DOM
childRef.value.someMethod() // 调用子组件方法
})
</script>
<template>
<input ref="inputRef" />
<ChildComponent ref="childRef" />
</template>
⚠️ 子组件需暴露方法:
<script setup>
function someMethod() { /* ... */ }
defineExpose({ someMethod })
</script>
6. Vue Router(v4)
| 用法 | 代码示例 |
|---|---|
| 路由链接 | <router-link to="/home">首页</router-link> |
| 路由视图 | <router-view /> |
| 编程式导航 | router.push('/user/1'), console.log(route.params.id) |
| 路由守卫 | router.beforeEach((to, from) => { if (to.meta.requiresAuth && !isLogin) return '/login' }) |
7. Pinia(状态管理)
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await fetch('/api/data')
this.data = await res.json()
}
}
})
使用方式:
<script setup>
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>{{ counter.count }}</p>
<p>{{ counter.double }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
8. 常用内置指令
| 指令 | 用法 | 说明 |
|---|---|---|
| v-if / v-show | v-if="visible", v-show="visible" | v-if 条件渲染(切换 DOM),v-show 切换 display |
| v-for | v-for="item in items" :key="item.id" | 列表渲染,必须加 :key |
| v-model | v-model="text", v-model:checked="flag" | 双向绑定,支持 .trim, .number, .lazy 修饰符 |
| v-on | @click="onClick", @keyup.enter="submit" | 事件绑定,支持按键修饰符 |
| v-bind | :href="url", :class="{ active: isActive }" | 属性绑定,支持对象/数组语法 |
9. Teleport & Suspense
| 特性 | 用法 | 说明 |
|---|---|---|
<Teleport> | <Teleport to="body"><div class="modal">弹窗</div></Teleport> | 将内容渲染到 DOM 任意位置 |
<Suspense> | <Suspense><template #default><AsyncComponent /></template><template #fallback><p>加载中...</p></template></Suspense> | 异步组件加载状态管理 |
10. 实用技巧与最佳实践
| 场景 | 推荐写法 |
|---|---|
| 防抖/节流 | 使用 lodash-es:import { debounce } from 'lodash-es'; const search = debounce(fn, 300) |
| 类型支持(TS) | <script setup lang="ts"> + interface User { id: number; name: string } + const user = ref<User>() |
| 环境变量 | 使用 import.meta.env:import.meta.env.MODE,VITE_API_URL 需以 VITE_ 开头 |
| 动态组件 | <component :is="currentTabComponent" /> |
| KeepAlive 缓存 | <KeepAlive><router-view v-if="$route.meta.keepAlive" /></KeepAlive> |
Vue 3 核心概念关系:
+------------------+
| Composition API |
| (setup, ref, |
| reactive, etc) |
+--------+---------+
|
v
+--------+---------+ +------------------+
| Reactive Data |<--->| Template |
| (响应式数据) | | (模板语法) |
+--------+---------+ +------------------+
|
v
+--------+---------+
| Lifecycle |
| Hooks |
+--------+---------+
|
v
+--------+---------+ +------------------+
| Component |<--->| Props / Emits |
| (组件) | | (通信) |
+------------------+ +------------------+
|
v
+------------------+
| Router + Pinia |
| (路由 + 状态) |
+------------------+
💡 提示:
- 所有代码示例基于
<script setup>语法糖。- 推荐搭配 Vite、TypeScript、ESLint、Prettier 使用。
- 使用
defineProps、defineEmits无需导入。- 响应式数据变更:
ref用.value,reactive直接修改属性。