目录
- [1.1 基础概念](#1.1 基础概念 "#11-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
- [MVVM 模式](#MVVM 模式 "#mvvm-%E6%A8%A1%E5%BC%8F")
- [1.2 项目创建(Vite)](#1.2 项目创建(Vite) "#12-%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BAvite")
- [1.3 模板语法](#1.3 模板语法 "#13-%E6%A8%A1%E6%9D%BF%E8%AF%AD%E6%B3%95")
- [2. 组件开发](#2. 组件开发 "#2-%E7%BB%84%E4%BB%B6%E5%BC%80%E5%8F%91")
- [2.1 组件基础](#2.1 组件基础 "#21-%E7%BB%84%E4%BB%B6%E5%9F%BA%E7%A1%80")
- [2.2 组件通信](#2.2 组件通信 "#22-%E7%BB%84%E4%BB%B6%E9%80%9A%E4%BF%A1")
- [Props(父 → 子)](#Props(父 → 子) "#props%E7%88%B6--%E5%AD%90")
- [Emits(子 → 父)](#Emits(子 → 父) "#emits%E5%AD%90--%E7%88%B6")
- v-model(双向绑定语法糖)
- [Provide / Inject(跨层级)](#Provide / Inject(跨层级) "#provide--inject%E8%B7%A8%E5%B1%82%E7%BA%A7")
- Ref(父访问子实例)
- [2.3 插槽](#2.3 插槽 "#23-%E6%8F%92%E6%A7%BD")
- [2.4 动态组件](#2.4 动态组件 "#24-%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6")
- [2.5 Teleport](#2.5 Teleport "#25-teleport")
- [2.6 自定义指令](#2.6 自定义指令 "#26-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8C%87%E4%BB%A4")
- [3. Composition API](#3. Composition API "#3-composition-api")
- [3.1 响应式 API](#3.1 响应式 API "#31-%E5%93%8D%E5%BA%94%E5%BC%8F-api")
- [3.2 依赖注入(provide / inject)](#3.2 依赖注入(provide / inject) "#32-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5provide--inject")
- [3.3 生命周期钩子](#3.3 生命周期钩子 "#33-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90")
- [3.4 组合式函数(Composables)](#3.4 组合式函数(Composables) "#34-%E7%BB%84%E5%90%88%E5%BC%8F%E5%87%BD%E6%95%B0composables")
- [3.5
<script setup>语法糖](#3.5 <script setup> 语法糖 "#35-script-setup-%E8%AF%AD%E6%B3%95%E7%B3%96")- 核心编译宏
- [
defineOptions与属性透传(inheritAttrs)](#defineOptions 与属性透传(inheritAttrs) "#defineoptions-%E4%B8%8E%E5%B1%9E%E6%80%A7%E9%80%8F%E4%BC%A0inheritattrs")
1.1 基础概念
MVVM 模式
MVVM 由三部分组成:M odel(模型)、V iew(视图)、V iewModel(视图模型)。
click / input / submit"] end subgraph ViewModel_Layer["⚙️ ViewModel 层"] direction TB VM1["📦 响应式数据
data / ref / reactive"] VM2["🔄 计算属性
computed"] VM3["👁️ 侦听器
watch"] VM4["🔗 生命周期钩子
mounted / updated"] VM5["🛠️ 方法
methods"] end subgraph Model_Layer["🗄️ Model 层"] direction TB M1["🌐 API 请求
axios / fetch"] M3["💡 业务逻辑
数据处理 / 校验 / 数据模型"] M4["🏪 状态管理
Vuex / Pinia"] end %% View → ViewModel V3 -- "① 用户操作触发" --> VM5 V1 -- "② v-model 双向绑定" --> VM1 %% ViewModel 内部依赖 VM1 -- "③-a 依赖数据变化
触发重新计算" --> VM2 VM1 -- "③-b 依赖数据变化
触发侦听器" --> VM3 VM4 -- "③-c 生命周期触发
初始化加载等" --> VM5 %% ViewModel → View VM1 -- "④ 数据驱动视图更新" --> V1 VM2 -- "⑤ 计算结果渲染到模板" --> V1 %% ViewModel → Model VM5 -- "⑥ 调用 API" --> M1 VM3 -- "⑦ 监听变化触发业务逻辑" --> M3 %% Model → ViewModel M1 -- "⑧ 返回数据 → 写入响应式变量" --> VM1 M4 -- "⑨ 状态变更通知 → 写入响应式变量" --> VM1 %% 自动触发渲染 M1 -. "⑧→④ 自动触发视图渲染 🔄" .-> V1 M4 -. "⑨→④ 自动触发视图渲染 🔄" .-> V1 style View_Layer fill:#E3F2FD,stroke:#1565C0,stroke-width:3px style ViewModel_Layer fill:#FFF3E0,stroke:#E65100,stroke-width:3px style Model_Layer fill:#E8F5E9,stroke:#2E7D32,stroke-width:3px style V1 fill:#BBDEFB,stroke:#1565C0,stroke-width:2px style V3 fill:#BBDEFB,stroke:#1565C0,stroke-width:2px style VM1 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style VM2 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style VM3 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style VM4 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style VM5 fill:#FFE0B2,stroke:#E65100,stroke-width:2px style M1 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px style M3 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px style M4 fill:#C8E6C9,stroke:#2E7D32,stroke-width:2px linkStyle 0 stroke:#D32F2F,stroke-width:2.5px linkStyle 1 stroke:#D32F2F,stroke-width:2.5px linkStyle 2 stroke:#FF6F00,stroke-width:2.5px linkStyle 3 stroke:#FF6F00,stroke-width:2.5px linkStyle 4 stroke:#FF6F00,stroke-width:2.5px linkStyle 5 stroke:#1565C0,stroke-width:2.5px linkStyle 6 stroke:#1565C0,stroke-width:2.5px linkStyle 7 stroke:#FF6F00,stroke-width:2.5px linkStyle 8 stroke:#FF6F00,stroke-width:2.5px linkStyle 9 stroke:#2E7D32,stroke-width:2.5px linkStyle 10 stroke:#2E7D32,stroke-width:2.5px linkStyle 11 stroke:#9C27B0,stroke-width:2px,stroke-dasharray:5 linkStyle 12 stroke:#9C27B0,stroke-width:2px,stroke-dasharray:5
三层职责与 Vue 中的映射:
| 缩写 | 全称 | 职责 | Vue 中的对应 |
|---|---|---|---|
| M | Model(模型) | 应用数据与业务逻辑。不关心数据如何展示,只负责存储和处理 | reactive()/ref() 声明的响应式状态、API 请求返回的数据、Pinia store |
| V | View(视图) | 用户看到的界面。不包含业务逻辑,只负责声明式地描述 UI 结构 | <template> 中的 HTML 模板、最终渲染出的真实 DOM |
| VM | ViewModel(视图模型) | M 与 V 之间的桥梁。监听 Model 变化自动更新 View,捕获 View 上的用户交互反向更新 Model | Vue 组件实例本身------编译器将模板转为渲染函数,响应式系统追踪依赖并触发更新 |
流程总结:
整个 MVVM 的运转可以概括为一个闭环:
- 用户交互驱动(V → VM): 用户在 View 层触发事件(click、input 等),通过
v-model或事件绑定将操作传递给 ViewModel 层的方法或响应式变量。 - ViewModel 内部联动(VM 内部): 响应式数据变化后,
computed自动重新计算派生值,watch触发副作用逻辑,生命周期钩子在适当时机执行。 - 业务处理(VM → M): ViewModel 调用 Model 层的 API 请求或业务逻辑函数,完成数据的增删改查。
- 数据回写(M → VM): Model 层返回结果写入响应式变量,或 Pinia/Vuex 状态变更通知 ViewModel。
- 视图自动更新(VM → V): 响应式系统检测到数据变化,自动触发虚拟 DOM diff,最小化更新真实 DOM,用户看到最新界面。
核心价值: 开发者只需关注数据(M)和模板(V) ,中间的同步、diff、DOM 操作全部由 Vue 的 ViewModel 层 自动完成。
v-model是:modelValue+@update:modelValue的编译时语法糖,本质仍由 VM 层协调,Vue 3 整体是单向数据流 + 双向绑定语法糖的设计。
1.2 项目创建(Vite)
基于原生 ES Module,毫秒级冷启动,HMR 不随项目规模变慢。
bash
# 推荐使用 create-vue(Vue 官方脚手架,底层基于 Vite)
npm create vue@latest
典型项目结构:
csharp
my-project/
├── public/ # 静态资源(不经过构建)
├── src/
│ ├── assets/ # 需构建处理的资源(图片、样式)
│ ├── components/ # 通用组件
│ ├── composables/ # 组合式函数
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia 状态管理
│ ├── views/ # 页面级组件
│ ├── App.vue
│ └── main.ts
├── index.html # 入口 HTML(Vite 以此为入口)
├── vite.config.ts
├── tsconfig.json
└── package.json
1.3 模板语法
插值与绑定
vue
<template>
<!-- 文本插值 -->
<span>{{ message }}</span>
<!-- 属性绑定(v-bind 简写 :) -->
<img :src="imgUrl" :alt="title" />
<!-- 动态绑定多个属性 -->
<div v-bind="attrs"></div>
<!-- 等价于 <div :id="attrs.id" :class="attrs.class" /> -->
<!-- 事件绑定(v-on 简写 @) -->
<button @click="submit">提交</button>
<input @keyup.enter="search" /> <!-- 按键修饰符 -->
<form @submit.prevent="save" /> <!-- 阻止默认行为 -->
</template>
条件渲染
vue
<template>
<!-- v-if:条件为 false 时 DOM 不存在(适合不频繁切换) -->
<div v-if="status === 'loading'">加载中</div>
<div v-else-if="status === 'error'">出错了</div>
<div v-else>{{ data }}</div>
<!-- v-show:始终渲染 DOM,通过 display 切换(适合频繁切换) -->
<div v-show="visible">我一直在 DOM 中</div>
</template>
| 指令 | DOM 行为 | 初始开销 | 切换开销 | 适用场景 |
|---|---|---|---|---|
v-if |
销毁/重建 | 低(不渲染) | 高 | 条件很少变化 |
v-show |
display: none |
高(始终渲染) | 低 | 频繁切换显示 |
列表渲染
vue
<template>
<!-- 数组遍历 -->
<li v-for="(item, index) in list" :key="item.id">
{{ index }}. {{ item.name }}
</li>
<!-- 对象遍历 -->
<div v-for="(value, key) in obj" :key="key">
{{ key }}: {{ value }}
</div>
<!-- v-for + v-if 不能同级使用,需用 <template> 包裹 -->
<template v-for="item in list" :key="item.id">
<li v-if="item.active">{{ item.name }}</li>
</template>
</template>
key的作用: 帮助 Vue 的 diff 算法识别节点身份,复用和重排已有元素而非重新创建。务必使用唯一业务 ID ,避免用index(排序/删除时会导致错误复用)。
2. 组件开发
组件是 Vue 的核心抽象单元------将 UI 拆分为独立、可复用的模块,每个组件封装自己的模板、逻辑和样式,通过明确的接口(props/emits)进行通信。
2.1 组件基础
组件定义与注册
Vue 3 推荐使用单文件组件(SFC) + <script setup> 语法,编译器自动处理注册,无需手动声明。
vue
<!-- MyButton.vue --- 单文件组件 -->
<template>
<button :class="type" @click="emit('click', $event)">
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{ type?: 'primary' | 'default' }>()
const emit = defineEmits<{ click: [e: MouseEvent] }>()
</script>
<style scoped>
.primary { background: #409eff; color: #fff; }
</style>
使用方式: 在 <script setup> 中导入即可直接在模板使用,无需注册。
vue
<template>
<MyButton type="primary" @click="save">保存</MyButton>
</template>
<script setup lang="ts">
import MyButton from './MyButton.vue'
</script>
SFC 的价值: 一个
.vue文件 = 模板 + 逻辑 + 样式,scoped实现样式隔离,<script setup>减少样板代码,编译器自动优化。
2.2 组件通信
Vue 组件间通信方式按场景选择,核心原则:props 向下,events 向上,跨层用 provide/inject。
Props(父 → 子)
父组件通过属性向子组件传递数据,子组件只读不可修改。
vue
<!-- Child.vue -->
<template>
<h2>{{ title }} ({{ count }})</h2>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0 // 类型声明的 props 通过 withDefaults 设置默认值
})
</script>
vue
<!-- Parent.vue -->
<Child title="订单" :count="orderCount" />
Emits(子 → 父)
子组件通过事件通知父组件,保持单向数据流。
vue
<!-- Child.vue -->
<template>
<button @click="remove(item.id)">删除</button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
update: [value: string]
delete: [id: number]
}>()
function remove(id: number) {
emit('delete', id) // 触发事件,父组件通过 @delete 监听
}
</script>
vue
<!-- Parent.vue -->
<Child @update="handleUpdate" @delete="handleDelete" />
v-model(双向绑定语法糖)
v-model 本质是 :modelValue + @update:modelValue 的简写,支持多个 v-model。
vue
<!-- SearchInput.vue -->
<template>
<input v-model="keyword" />
<select v-model="status">
<option value="all">全部</option>
<option value="active">启用</option>
</select>
</template>
<script setup lang="ts">
const keyword = defineModel<string>() // 默认 v-model
const status = defineModel<string>('status') // v-model:status
</script>
vue
<!-- Parent.vue --- 语法糖写法 -->
<SearchInput v-model="keyword" v-model:status="currentStatus" />
上面的 v-model:status 等价于展开写法:
vue
<!-- Parent.vue --- 展开写法(与上方完全等价) -->
<SearchInput
v-model="keyword"
:status="currentStatus"
@update:status="currentStatus = $event"
/>
v-model:status编译后就是:status+@update:status,defineModel('status')内部帮你处理了 props 接收和 emit 触发。
Provide / Inject(跨层级)
祖先组件提供数据,任意后代组件注入,避免 props 逐层透传。
vue
<!-- 祖先组件 -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref<'light' | 'dark'>('light')
provide('theme', theme) // key-value 形式提供
</script>
vue
<!-- 任意深度的后代组件 -->
<template>
<div :class="theme">当前主题:{{ theme }}</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import type { Ref } from 'vue'
const theme = inject<Ref<'light' | 'dark'>>('theme') // 注入
</script>
使用 InjectionKey 实现类型安全(推荐):
字符串 key 容易拼错且无类型推导,推荐抽取 InjectionKey 常量:
typescript
// keys.ts --- 统一管理 injection key
import type { InjectionKey, Ref } from 'vue'
export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
typescript
// 祖先组件中 provide
import { ThemeKey } from './keys'
provide(ThemeKey, theme) // TS 自动校验 value 类型
// 后代组件中 inject
import { ThemeKey } from './keys'
const theme = inject(ThemeKey) // 自动推导为 Ref<'light' | 'dark'> | undefined
适用场景: 主题、国际化、全局配置等需要跨多层传递但不适合放 Pinia 的数据。
Ref(父访问子实例)
通过模板 ref 获取子组件实例,直接调用子组件暴露的方法或属性。
defineExpose 的作用: 在 <script setup> 中,组件内部的变量和方法默认对外不可见 (与 Options API 不同)。必须通过 defineExpose 显式声明哪些内容允许父组件通过 ref 访问。未暴露的内容,父组件拿到 ref 后也无法调用。
vue
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const secret = ref('隐藏数据') // 未暴露,父组件无法访问
function reset() { count.value = 0 }
// 只有 count 和 reset 对外可见,secret 外部不可访问
defineExpose({ count, reset })
</script>
vue
<!-- Parent.vue -->
<template>
<Child ref="childRef" />
<button @click="resetChild">重置子组件</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref<InstanceType<typeof Child>>()
function resetChild() {
childRef.value?.reset() // 调用子组件通过 defineExpose 暴露的 reset 方法
}
</script>
注意: 模板 ref 访问破坏了组件封装性,仅在表单校验、弹窗控制等必要场景使用。
2.3 插槽
插槽让父组件向子组件内部注入模板片段,实现布局和内容的解耦。
默认插槽
子组件用 <slot /> 占位,父组件传入内容替换。
vue
<!-- Card.vue -->
<template>
<div class="card">
<slot /> <!-- 父组件传入的内容渲染在这里 -->
</div>
</template>
vue
<Card>
<p>这段内容会替换 slot 占位</p>
</Card>
具名插槽
多个插槽通过 name 区分,父组件用 v-slot:name(简写 #name)指定。
vue
<!-- Layout.vue -->
<template>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</template>
vue
<Layout>
<template #header>
<h1>页面标题</h1>
</template>
<p>默认插槽内容(main 区域)</p>
<template #footer>
<span>© 2026</span>
</template>
</Layout>
作用域插槽
子组件通过 slot 向父组件回传数据,父组件拿到数据后自定义渲染。
vue
<!-- DataList.vue -->
<script setup lang="ts">
defineProps<{ items: { id: number; name: string }[] }>()
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="item.id" /> <!-- 回传数据 -->
</li>
</ul>
</template>
vue
<!-- 父组件自定义每一行的渲染方式 -->
<DataList :items="list">
<template #default="{ item, index }">
<span>{{ index }}. {{ item.name }}</span>
<button @click="remove(item.id)">删除</button>
</template>
</DataList>
作用域插槽的价值: 子组件负责数据遍历和逻辑,父组件负责 UI 呈现,实现逻辑与视图的分离。常见于表格列自定义、列表项渲染等场景。
2.4 动态组件
<component :is>
根据变量动态切换渲染的组件,适用于 Tab 切换、多表单步骤等场景。
vue
<template>
<button v-for="tab in tabs" :key="tab.label" @click="current = tab.comp">
{{ tab.label }}
</button>
<component :is="current" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'
const tabs = [
{ label: '基本信息', comp: TabA },
{ label: '详细配置', comp: TabB },
{ label: '操作日志', comp: TabC },
]
const current = shallowRef(tabs[0].comp) // shallowRef 避免深度代理组件对象
</script>
<keep-alive>
缓存被切走的组件实例,切回时保留状态(表单输入、滚动位置等),避免重新创建和销毁。
vue
<keep-alive :include="['TabA', 'TabB']" :max="5">
<component :is="current" />
</keep-alive>
| 属性 | 说明 |
|---|---|
include |
只缓存匹配的组件(名称或正则) |
exclude |
排除不缓存的组件 |
max |
最大缓存实例数,超出时销毁最久未使用的(LRU) |
被 keep-alive 缓存的组件可使用两个专属生命周期:
vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 组件从缓存中被激活(切回)时触发,可用于刷新数据
})
onDeactivated(() => {
// 组件被缓存(切走)时触发,可用于清理定时器
})
</script>
典型场景: 后台管理的多 Tab 页面切换------用户在 Tab A 填了一半表单,切到 Tab B 再切回来,数据不丢失。
defineAsyncComponent(异步组件)
将组件的 JS 代码从主包中分离,用到时才加载,减少首屏体积。Vite 构建时会自动将其拆为独立 chunk。
typescript
import { defineAsyncComponent } from 'vue'
// 基本用法:传入返回 import() 的工厂函数
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
// 完整配置:加载状态、超时、错误处理
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner, // 加载中显示的组件
errorComponent: ErrorBlock, // 加载失败显示的组件
delay: 200, // 延迟 200ms 后才显示 loading(避免闪烁)
timeout: 10000, // 超过 10s 视为超时,显示 errorComponent
})
vue
<!-- 在模板中像普通组件一样使用,Vue 自动处理懒加载 -->
<template>
<HeavyChart v-if="showChart" :data="chartData" />
</template>
适用场景: 大型图表、富文本编辑器、PDF 预览等体积较大且非首屏必需的组件。与路由懒加载(
() => import('./views/xxx.vue'))原理相同,区别在于异步组件是组件级别的按需加载。
2.5 Teleport
将组件模板的一部分渲染到DOM 树的其他位置 (如 body),解决弹窗/浮层被父组件 overflow: hidden 或 z-index 影响的问题。
vue
<template>
<button @click="visible = true">打开弹窗</button>
<!-- 内容渲染到 body 下,而非当前组件 DOM 内 -->
<Teleport to="body">
<div v-if="visible" class="modal-overlay">
<div class="modal">
<p>弹窗内容</p>
<button @click="visible = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
</script>
| 属性 | 说明 |
|---|---|
to |
CSS 选择器或 DOM 元素,指定渲染目标(如 "body"、"#modal-root") |
disabled |
为 true 时禁用传送,内容回到组件原位 |
逻辑上仍属于当前组件(props/emits/provide 照常工作),只是 DOM 位置变了。
2.6 自定义指令
封装对 DOM 的底层操作为可复用指令,命名 v-xxx。
typescript
// directives/vFocus.ts
import type { Directive } from 'vue'
export const vFocus: Directive = {
mounted(el: HTMLElement) {
el.focus() // 元素挂载后自动聚焦
}
}
vue
<template>
<input v-focus />
</template>
<script setup lang="ts">
import { vFocus } from '@/directives/vFocus'
</script>
指令生命周期钩子:
| 钩子 | 触发时机 |
|---|---|
created |
元素属性/事件绑定前 |
beforeMount |
插入 DOM 前 |
mounted |
插入 DOM 后 ✅ 最常用 |
beforeUpdate |
组件更新前 |
updated |
组件更新后 |
beforeUnmount |
卸载前 |
unmounted |
卸载后 |
带参数的实际示例(权限指令):
typescript
// directives/vPermission.ts
import type { Directive } from 'vue'
export const vPermission: Directive<HTMLElement, string> = {
mounted(el, binding) {
// binding.value 就是 v-permission="'admin'" 中的 'admin'
const userRole = getUserRole()
if (userRole !== binding.value) {
el.parentNode?.removeChild(el) // 无权限则移除元素
}
}
}
vue
<button v-permission="'admin'">仅管理员可见</button>
3. Composition API
Composition API 是 Vue 3 的核心编程范式,以函数 为基本组织单位,替代 Options API 的 data/methods/computed/watch 分散写法,使相关逻辑聚合在一起,便于复用和维护。
3.1 响应式 API
ref
包装任意类型 为响应式数据。JS/TS 中通过 .value 读写,模板中自动解包。包装对象时内部调用 reactive 实现深层响应。
typescript
import { ref } from 'vue'
const count = ref(0) // 基本类型
const user = ref<User | null>(null) // 对象类型,支持泛型
count.value++ // JS 中需要 .value
// 嵌套对象同样响应式(内部自动 reactive)
const config = ref({ theme: { color: 'blue' } })
config.value.theme.color = 'red' // ✅ 视图更新
config.value = { theme: { color: 'green' } } // ✅ 整体替换也响应式
// 数组同样深层响应式
const list = ref([{ id: 1, name: '张三' }, { id: 2, name: '李四' }])
list.value.push({ id: 3, name: '王五' }) // ✅ 新增元素,视图更新
list.value[0].name = '赵六' // ✅ 修改元素属性,视图更新
list.value = list.value.filter(i => i.id !== 2) // ✅ 整体替换,视图更新
自动解包规则 & 注意事项:
| 场景 | 需要 .value? |
说明 |
|---|---|---|
模板 {{ count }} |
否 | 自动解包 |
| JS/TS 代码 | 是 | count.value++ |
嵌入 reactive 对象 |
否 | reactive({ count }).count++ |
| 放入数组 / Map | 是 | reactive([ref(1)])[0].value |
解构 .value |
--- | 丢失响应性,需 toRefs() 转换 |
reactive
将对象转为深层响应式代理,访问属性无需 .value。不能用于基本类型,且不能整体替换(会丢失响应性)。
typescript
import { reactive } from 'vue'
const form = reactive({
name: '',
age: 0,
address: { city: '', zip: '' } // 嵌套对象也是响应式
})
form.name = '张三' // 直接赋值,无需 .value
form.address.city = '深圳' // 深层属性也是响应式
ref vs reactive 选择:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基本类型(string / number / boolean) | ref |
reactive 不支持基本类型 |
| 可能被整体替换的对象 | ref |
reactive 重新赋值会丢失响应性 |
| 表单等字段固定的复杂对象 | reactive |
无需 .value,代码更简洁 |
| composable 函数返回值 | ref |
解构时不丢失响应性 |
computed
基于响应式依赖自动缓存的派生值,依赖不变则不重新计算。
typescript
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(3)
// 只读计算属性
const total = computed(() => price.value * quantity.value)
// 可写计算属性(少用)
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last ?? ''
}
})
与方法的区别:
computed有缓存,多次访问只在依赖变化时重新计算;方法每次调用都重新执行。
watch
监听特定响应式数据,变化时执行回调。适合需要旧值对比 或有条件执行的场景。
typescript
import { ref, watch } from 'vue'
const keyword = ref('')
// 监听单个 ref
watch(keyword, (newVal, oldVal) => {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
})
// 监听多个源
watch([keyword, page], ([newKeyword, newPage], [oldKeyword, oldPage]) => {
fetchList(newKeyword, newPage)
})
// 监听 reactive 对象的某个属性(需用 getter 函数)
const form = reactive({ name: '', age: 0 })
watch(
() => form.name,
(newName) => { validate(newName) }
)
// 常用选项
watch(keyword, handler, {
immediate: true, // 创建时立即执行一次
deep: true, // 深层监听(reactive 对象默认深层,ref 对象需手动开启)
flush: 'post', // 在 DOM 更新后执行回调(默认 'pre')
})
watchEffect
自动追踪回调中用到的所有响应式依赖,不需要指定监听源。适合"用了什么就监听什么"的场景。
typescript
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
// 回调中访问了 keyword 和 page,两者变化都会重新执行
const stop = watchEffect(() => {
fetchList(keyword.value, page.value)
})
stop() // 手动停止监听(组件卸载时自动停止)
watch vs watchEffect 对比:
| 维度 | watch |
watchEffect |
|---|---|---|
| 监听源 | 需显式指定 | 自动追踪回调中的依赖 |
| 旧值访问 | ✅ (newVal, oldVal) |
❌ 无旧值 |
| 首次执行 | 默认不执行(immediate: true 开启) |
默认立即执行 |
| 适用场景 | 需要旧值对比、条件触发 | 依赖多且不需要旧值 |
nextTick
Vue 的 DOM 更新是异步批量 的,修改数据后 DOM 不会立即更新。nextTick 等待 DOM 更新完成后执行回调。
typescript
import { ref, nextTick } from 'vue'
const show = ref(false)
async function expand() {
show.value = true
// 此时 DOM 尚未更新,拿不到新元素
await nextTick()
// DOM 已更新,可安全操作
document.querySelector('.detail')?.scrollIntoView()
}
响应式工具函数
| 函数 | 作用 | 典型场景 |
|---|---|---|
toRef(obj, key) |
将 reactive 对象的单个属性转为 ref | 传递单个属性给 composable |
toRefs(obj) |
将 reactive 对象的所有属性转为 ref | 解构 reactive 不丢失响应性 |
toRaw(proxy) |
返回代理的原始对象 | 传给第三方库(避免代理副作用) |
shallowRef(val) |
只有 .value 替换时触发更新,深层属性变化不触发 |
大型对象 / 组件引用 |
shallowReactive(obj) |
只有顶层属性变化触发更新 | 扁平配置对象 |
markRaw(obj) |
标记对象永不被代理 | 第三方类实例(echarts、地图等) |
typescript
import { reactive, toRefs, toRaw, shallowRef, markRaw } from 'vue'
// toRefs:解构不丢失响应性
const state = reactive({ name: '张三', age: 20 })
const { name, age } = toRefs(state) // name、age 都是 Ref
name.value = '李四' // state.name 同步变化
// shallowRef:大型对象只在整体替换时触发更新
const tableData = shallowRef<Row[]>([])
tableData.value[0].name = '新名字' // ❌ 不触发更新
tableData.value = [...tableData.value] // ✅ 整体替换才触发
// markRaw:排除不需要响应式的对象
const chart = markRaw(echarts.init(el))
3.2 依赖注入(provide / inject)
已在 [2.2 组件通信 --- Provide / Inject](#2.2 组件通信 — Provide / Inject "#provide--inject%E8%B7%A8%E5%B1%82%E7%BA%A7") 中详细介绍,包含基本用法和 InjectionKey 类型安全写法。
核心要点回顾:
provide(key, value)在祖先组件提供数据inject(key)在任意后代组件注入- 推荐使用
InjectionKey<T>常量管理 key,实现自动类型推导 - 适用于主题、国际化等跨层级共享数据的场景
3.3 生命周期钩子
Composition API 中通过 onXxx 函数注册生命周期回调,对应组件从创建到销毁的各个阶段。
DOM 已挂载,可访问 DOM / 发请求"] C --> D["onBeforeUpdate"] D --> E["onUpdated
DOM 已更新"] E --> D C --> F["onBeforeUnmount"] F --> G["onUnmounted
组件已销毁,清理副作用"]
typescript
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// setup 本身等价于 beforeCreate + created,无需对应钩子
onMounted(() => {
// DOM 已渲染,适合:获取 DOM 引用、发起初始请求、初始化第三方库
initChart()
fetchData()
})
onUpdated(() => {
// 响应式数据变化导致 DOM 更新后触发
// 注意:避免在此修改响应式数据,可能导致无限循环
})
onBeforeUnmount(() => {
// 组件即将销毁,适合:清除定时器、取消订阅、销毁第三方库实例
clearInterval(timer)
chart?.dispose()
})
Options API 与 Composition API 钩子映射:
| Options API | Composition API | 说明 |
|---|---|---|
beforeCreate |
setup() 本身 |
setup 在所有 Options API 钩子之前执行 |
created |
setup() 本身 |
响应式数据已就绪,但 DOM 未挂载 |
beforeMount |
onBeforeMount |
DOM 挂载前 |
mounted |
onMounted |
DOM 已挂载 ✅ |
beforeUpdate |
onBeforeUpdate |
数据变化,DOM 更新前 |
updated |
onUpdated |
DOM 已更新 |
beforeUnmount |
onBeforeUnmount |
组件销毁前 |
unmounted |
onUnmounted |
组件已销毁 |
常用原则: 初始化请求放
onMounted(而非 setup),清理工作放onBeforeUnmount。同一钩子可多次调用,按注册顺序执行。
3.4 组合式函数(Composables)
将相关联的响应式状态 + 逻辑 提取为独立函数,实现跨组件复用。命名约定以 use 开头。
typescript
// composables/useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<string>
isFetching: Ref<boolean>
execute: () => Promise<void>
}
export function useFetch<T>(url: string | Ref<string>): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref('')
const isFetching = ref(false)
async function execute() {
isFetching.value = true
error.value = ''
try {
const resolvedUrl = typeof url === 'string' ? url : url.value
const res = await fetch(resolvedUrl)
data.value = await res.json()
} catch (e: any) {
error.value = e.message
} finally {
isFetching.value = false
}
}
execute() // 创建时自动执行一次
return { data, error, isFetching, execute }
}
vue
<!-- 在组件中使用 -->
<template>
<div v-if="isFetching">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'
const { data, error, isFetching } = useFetch<Item[]>('/api/items')
</script>
Composable 设计原则:
| 原则 | 说明 |
|-------------|----------------------------------------|-------------|
| 单一职责 | 一个 composable 只解决一类问题(如请求、分页、表单校验) |
| 返回 ref | 返回值使用 ref 而非 reactive,调用方解构时不丢失响应性 |
| 命名 useXxx | 约定以 use 开头,表明这是一个组合式函数 |
| 可接收 ref 参数 | 参数支持 `string | Ref`,提高灵活性 |
与 Mixin 的对比: Options API 时代用 Mixin 复用逻辑,但存在命名冲突、来源不明、隐式依赖等问题。Composable 通过显式导入和返回值,完全解决了这些问题。
3.5 <script setup> 语法糖
<script setup> 是 Composition API 在 SFC 中的编译时语法糖,编译器自动处理导出、注册、类型推导,减少样板代码。
核心编译宏
编译宏无需导入,编译器自动识别:
| 宏 | 作用 | 示例 |
|---|---|---|
defineProps |
声明 props | defineProps<{ title: string }>() |
defineEmits |
声明 emits | defineEmits<{ change: [value: string] }>() |
defineExpose |
暴露实例属性/方法 | defineExpose({ reset }) |
defineModel |
声明 v-model 双向绑定 | defineModel<string>('status') |
withDefaults |
为类型声明的 props 设置默认值 | withDefaults(defineProps<P>(), { count: 0 }) |
defineOptions |
声明组件选项(如 name / inheritAttrs) | defineOptions({ name: 'MyComp' }) |
<script setup> vs 普通 <script>
vue
<!-- ❌ 普通 script:需要手动 return、注册组件 -->
<script lang="ts">
import { ref, defineComponent } from 'vue'
import MyButton from './MyButton.vue'
export default defineComponent({
components: { MyButton },
setup() {
const count = ref(0)
function increment() { count.value++ }
return { count, increment } // 必须手动 return
}
})
</script>
<!-- ✅ script setup:顶层变量/导入组件自动暴露给模板 -->
<script setup lang="ts">
import { ref } from 'vue'
import MyButton from './MyButton.vue' // 自动注册,模板中直接用
const count = ref(0)
function increment() { count.value++ }
// 无需 return,顶层声明自动可在模板中使用
</script>
defineOptions 与属性透传(inheritAttrs)
默认情况下,父组件传给子组件的未声明为 props 的属性 (如 class、style、id、data-*)会自动透传到子组件的根元素上。通过 defineOptions({ inheritAttrs: false }) 可关闭自动透传,改用 useAttrs() 手动控制。
vue
<!-- BaseInput.vue -->
<template>
<!-- 关闭自动透传后,attrs 不会自动加到根 div 上 -->
<div class="input-wrapper">
<!-- 手动绑定到指定元素 -->
<input v-bind="attrs" />
</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
defineOptions({
name: 'BaseInput', // 组件名(keep-alive include 匹配用)
inheritAttrs: false // 关闭自动透传
})
const attrs = useAttrs() // 获取所有透传属性
</script>
vue
<!-- 父组件使用 -->
<template>
<!-- class、placeholder、@focus 都会透传到 BaseInput 内部的 <input> 上 -->
<BaseInput class="custom" placeholder="请输入" @focus="onFocus" />
</template>
| 场景 | inheritAttrs |
效果 |
|---|---|---|
| 单根元素组件(默认) | true |
attrs 自动添加到根元素 |
| 需要将 attrs 绑定到非根元素 | false + v-bind="attrs" |
手动控制透传目标 |
| 多根元素组件 | --- | Vue 警告,必须手动 v-bind="$attrs" 指定 |