外观
Vue 3 笔记
约 3444 字大约 11 分钟
2025-08-16
一、Vue 3 基础
1.1 什么是 Vue 3?
Vue 3 是 Vue.js 框架的最新主要版本,于 2020 年 9 月 18 日正式发布(代号 "One Piece")。它是对 Vue 2 的全面升级,提供了更好的性能、更小的包体积、更强大的功能和更好的 TypeScript 支持。
1.2 Vue 3 的核心优势
性能提升
- 打包大小减少 41%
- 初次渲染快 55%,更新渲染快 133%
- 内存占用减少 54%
源码升级
- 使用 Proxy 代替 defineProperty 实现响应式系统
- 重写虚拟 DOM 的实现和 Tree-Shaking 支持
- 更好的 TypeScript 支持
新特性
- Composition API(组合式 API):更灵活的代码组织方式
- 新的内置组件:
Fragment、Teleport、Suspense - 优化的 TypeScript 支持
- 更好的逻辑复用机制
1.3 安装与项目创建
使用 Vite 创建 Vue 3 项目(推荐)
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev使用 Vue CLI 创建 Vue 3 项目
npm install -g @vue/cli
vue create my-vue-app
# 选择 "Vue 3" 预设
cd my-vue-app
npm run serve项目结构概览
my-vue-app/
├── node_modules/ # 依赖包
├── public/ # 静态资源
├── src/ # 源代码
│ ├── assets/ # 静态资源
│ ├── components/ # 组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html # HTML 模板
└── package.json二、Composition API 核心
2.1 setup 函数
setup 函数是 Composition API 的入口点
import { ref } from 'vue'
export default {
setup() {
// Composition API 代码写在这里
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
}setup 函数的特点:
- 在 beforeCreate 之前执行
- this 是 undefined(不能访问 Vue 2 的选项 API)
- 接收两个参数:
props和context - 返回的对象中的属性和方法可以在模板中直接使用
- 尽量不要与 Vue 2.x 配置混用
setup 函数参数
export default {
props: {
title: String
},
setup(props, context) {
// props: 组件外部传递过来且内部声明接收的属性
console.log(props.title)
// context: 上下文对象
// context.attrs: 未在 props 中声明的属性
// context.slots: 插槽内容
// context.emit: 触发自定义事件
return {
// 返回的内容可在模板中使用
}
}
}2.2 ref 与 reactive
ref 函数:处理基本类型数据
import { ref } from 'vue'
export default {
setup() {
// 创建响应式数据
const count = ref(0)
const name = ref('Vue 3')
// 修改响应式数据
function increment() {
count.value++
}
return {
count,
name,
increment
}
}
}模板中使用 ref
<template>
<div>
<p>计数: {{ count }}</p>
<p>名称: {{ name }}</p>
<button @click="increment">增加</button>
</div>
</template>注意:
- JS 中操作数据需要
.value(count.value++) - 模板中读取数据不需要
.value({{ count }}) - ref 也可以处理对象类型数据(内部会自动转为 reactive)
reactive 函数:处理对象/数组类型数据
import { reactive } from 'vue'
export default {
setup() {
// 创建响应式对象
const user = reactive({
name: '张三',
age: 18,
hobbies: ['阅读', '编程']
})
// 修改响应式对象
function updateAge() {
user.age++
}
return {
user,
updateAge
}
}
}模板中使用 reactive
<template>
<div>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<ul>
<li v-for="hobby in user.hobbies" :key="hobby">{{ hobby }}</li>
</ul>
<button @click="updateAge">增加年龄</button>
</div>
</template>注意:
- reactive 定义的对象,操作和读取数据都不需要
.value - 不能用于基本类型数据
ref 与 reactive 对比
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象 | 对象、数组 |
| 操作数据 | 需要 .value | 不需要 .value |
| 模板使用 | 不需要 .value | 不需要 .value |
| 原理 | Object.defineProperty | Proxy |
2.3 computed 与 watch
computed:计算属性
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('张')
const lastName = ref('三')
// 计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})
// 带 setter 的计算属性
const fullNameWithSetter = computed({
get() {
return firstName.value + lastName.value
},
set(newValue) {
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[1] || ''
}
})
return {
firstName,
lastName,
fullName,
fullNameWithSetter
}
}
}watch:监听响应式数据变化
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 监听多个 ref
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})
// 监听 reactive 对象的属性
const user = reactive({ name: '张三', age: 18 })
watch(() => user.age, (newVal, oldVal) => {
console.log(`user.age 从 ${oldVal} 变为 ${newVal}`)
})
return {
count,
name,
user
}
}
}watchEffect:自动追踪依赖
import { ref, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const double = ref(0)
// 自动追踪依赖,当 count 变化时执行
watchEffect(() => {
double.value = count.value * 2
console.log(`count: ${count.value}, double: ${double.value}`)
})
function increment() {
count.value++
}
return {
count,
double,
increment
}
}
}三、模板语法与指令
3.1 基本指令
v-model:双向数据绑定
<template>
<div>
<input v-model="message" placeholder="请输入">
<p>输入内容: {{ message }}</p>
<!-- 修饰符 -->
<input v-model.trim="trimmed" placeholder="自动去除两端空格">
<input v-model.number="age" type="number" placeholder="转换为数字">
<input v-model.lazy="lazy" placeholder="失焦时更新">
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const message = ref('')
const trimmed = ref('')
const age = ref(0)
const lazy = ref('')
return {
message,
trimmed,
age,
lazy
}
}
}
</script>v-if 与 v-show:条件渲染
<template>
<div>
<button @click="show = !show">切换显示</button>
<!-- v-if:条件满足时渲染元素,不满足时从 DOM 中移除 -->
<p v-if="show">v-if 显示的内容</p>
<!-- v-show:通过 CSS 的 display 属性切换 -->
<p v-show="show">v-show 显示的内容</p>
<!-- v-else-if 和 v-else -->
<div v-if="type === 'A'">
类型 A
</div>
<div v-else-if="type === 'B'">
类型 B
</div>
<div v-else>
其他类型
</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const show = ref(true)
const type = ref('A')
return {
show,
type
}
}
}
</script>v-for:列表渲染
<template>
<div>
<ul>
<!-- 基本用法 -->
<li v-for="(item, index) in items" :key="index">
{{ index }} - {{ item }}
</li>
<!-- 遍历对象 -->
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
<!-- 带 key 的重要性 -->
<li v-for="item in itemsWithId" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const items = ref(['苹果', '香蕉', '橙子'])
const user = ref({
name: '张三',
age: 18,
city: '北京'
})
const itemsWithId = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
])
return {
items,
user,
itemsWithId
}
}
}
</script>3.2 事件处理
基本事件处理
<template>
<div>
<button @click="increment">点击增加 ({{ count }})</button>
<button @click="decrement">点击减少</button>
<!-- 事件修饰符 -->
<button @click.stop="doThis">阻止事件冒泡</button>
<button @click.prevent="submit">阻止默认行为</button>
<button @click.once="doOnce">只触发一次</button>
<form @submit.prevent="onSubmit">
<input type="text" @keyup.enter="submit">
</form>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function onSubmit() {
console.log('表单提交')
}
return {
count,
increment,
decrement,
onSubmit
}
}
}
</script>事件传参
<template>
<div>
<button @click="say('hello')">Say hello</button>
<button @click="say('world')">Say world</button>
<!-- 保留原始事件对象 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
function say(message) {
alert(message)
}
function warn(message, event) {
// 现在我们可以访问原生事件
if (event) event.preventDefault()
alert(message)
}
return {
say,
warn
}
}
}
</script>四、组件开发
4.1 组件创建与注册
创建组件
<!-- components/MyComponent.vue -->
<template>
<div class="my-component">
<h2>{{ title }}</h2>
<p>{{ content }}</p>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
required: true
},
content: {
type: String,
default: '默认内容'
}
},
setup(props) {
console.log('组件标题:', props.title)
return {
// 可以返回额外的属性和方法
}
}
})
</script>
<style scoped>
.my-component {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 4px;
}
</style>组件注册与使用
<!-- App.vue -->
<template>
<div id="app">
<MyComponent
title="欢迎使用 Vue 3"
content="这是一个示例组件"
/>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import MyComponent from './components/MyComponent.vue'
export default defineComponent({
components: {
MyComponent
},
setup() {
return {
// 组件逻辑
}
}
})
</script>4.2 组件通信
父组件 → 子组件:Props
<!-- ParentComponent.vue -->
<template>
<ChildComponent
:message="parentMessage"
:count="count"
:is-active="isActive"
/>
</template>
<script>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
setup() {
const parentMessage = ref('来自父组件的消息')
const count = ref(0)
const isActive = ref(true)
return {
parentMessage,
count,
isActive
}
}
}
</script><!-- ChildComponent.vue -->
<template>
<div>
<p>消息: {{ message }}</p>
<p>计数: {{ count }}</p>
<p>状态: {{ isActive ? '激活' : '未激活' }}</p>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: false
}
}
})
</script>子组件 → 父组件:自定义事件
<!-- ChildComponent.vue -->
<template>
<div>
<input v-model="localMessage" @input="updateMessage">
<button @click="notifyParent">通知父组件</button>
</div>
</template>
<script>
import { ref, watch } from 'vue'
export default {
emits: ['update:message', 'notify'], // 声明触发的事件
props: {
message: String
},
setup(props, { emit }) {
const localMessage = ref(props.message)
// 当组件接收的新 props 变化时更新
watch(() => props.message, (newVal) => {
localMessage.value = newVal
})
function updateMessage() {
// 触发自定义事件,更新父组件的值
emit('update:message', localMessage.value)
}
function notifyParent() {
emit('notify', {
time: new Date(),
message: '子组件通知'
})
}
return {
localMessage,
updateMessage,
notifyParent
}
}
}
</script><!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent
:message="parentMessage"
@update:message="parentMessage = $event"
@notify="handleNotification"
/>
<p>父组件消息: {{ parentMessage }}</p>
</div>
</template>
<script>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
setup() {
const parentMessage = ref('初始消息')
function handleNotification(data) {
console.log('收到通知:', data)
alert(`收到通知: ${data.message},时间: ${data.time}`)
}
return {
parentMessage,
handleNotification
}
}
}
</script>跨组件通信:provide/inject
<!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent />
</div>
</template>
<script>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
setup() {
const theme = ref('light')
const user = {
name: '张三',
age: 18
}
// 提供数据
provide('theme', theme)
provide('user', user)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return {
toggleTheme
}
}
}
</script><!-- ChildComponent.vue -->
<template>
<div :class="theme">
<GrandChildComponent />
</div>
</template>
<script>
import { inject } from 'vue'
import GrandChildComponent from './GrandChildComponent.vue'
export default {
components: { GrandChildComponent },
setup() {
// 注入数据
const theme = inject('theme')
const user = inject('user')
return {
theme
}
}
}
</script><!-- GrandChildComponent.vue -->
<template>
<div>
<p>当前主题: {{ theme }}</p>
<p>用户信息: {{ user.name }} ({{ user.age }}岁)</p>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
// 注入数据
const theme = inject('theme')
const user = inject('user')
return {
theme,
user
}
}
}
</script>4.3 组件生命周期
Composition API 生命周期钩子
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
// 常用于初始化操作、获取 DOM 元素等
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
})
onBeforeUnmount(() => {
console.log('组件卸载前')
// 常用于清理操作,如移除事件监听器、清除定时器等
})
onUnmounted(() => {
console.log('组件已卸载')
})
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误', err, info)
// 返回 false 可阻止错误继续向上传播
return false
})
onRenderTracked((event) => {
console.log('响应式依赖被追踪', event)
})
onRenderTriggered((event) => {
console.log('响应式依赖更新触发渲染', event)
})
return {
// ...
}
}
}与 Options API 生命周期对比
| Composition API | Options API | 执行时机 |
|---|---|---|
| setup() | - | 组件创建前(替代 beforeCreate 和 created) |
| onBeforeMount | beforeMount | 组件挂载前 |
| onMounted | mounted | 组件挂载后 |
| onBeforeUpdate | beforeUpdate | 数据更新前 |
| onUpdated | updated | 数据更新后 |
| onBeforeUnmount | beforeDestroy | 组件卸载前 |
| onUnmounted | destroyed | 组件卸载后 |
| onErrorCaptured | errorCaptured | 捕获子组件错误时 |
五、Vue Router 4
5.1 安装与配置
安装 Vue Router 4
npm install vue-router@4基本路由配置
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Contact from '../views/Contact.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/contact',
name: 'Contact',
component: Contact
},
// 重定向
{
path: '/home',
redirect: '/'
},
// 404 页面
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router在 main.js 中使用路由
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')5.2 路由使用
导航链接
<template>
<div>
<!-- 使用 router-link 组件进行导航 -->
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link> |
<router-link to="/contact">联系</router-link>
</nav>
<!-- 路由出口 -->
<router-view />
</div>
</template>编程式导航
import { useRouter } from 'vue-router'
export default {
setup() {
const router = useRouter()
function goToAbout() {
// 字符串路径
router.push('/about')
// 带查询参数,结果是 /about?plan=private
router.push({ path: '/about', query: { plan: 'private' }})
// 命名的路由
router.push({ name: 'Contact', params: { id: '123' }})
// 替换当前历史记录
router.replace('/about')
}
return {
goToAbout
}
}
}路由参数
// 配置带参数的路由
const routes = [
{
path: '/users/:id',
name: 'User',
component: User
},
{
path: '/posts/:postId/comments/:commentId',
component: Comment
}
]
// 在组件中获取参数
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
// 获取路径参数
console.log(route.params.id) // 对应 :id
// 获取查询参数
console.log(route.query.plan) // 对应 ?plan=private
return {
// ...
}
}
}六、Pinia 状态管理
6.1 安装与配置
安装 Pinia
npm install pinia创建 Pinia 实例
// src/main.js
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')6.2 定义 Store
创建 store
// src/stores/counter.js
import { defineStore } from 'pinia'
// 定义 store
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: '计数器'
}),
getters: {
doubleCount: (state) => state.count * 2,
// 带参数的 getter
countPlus: (state) => (amount) => state.count + amount
},
actions: {
increment() {
this.count++
},
// 异步操作
async fetchData() {
const response = await fetch('https://api.example.com/data')
const data = await response.json()
this.count = data.value
}
}
})6.3 使用 Store
在组件中使用 store
<template>
<div>
<p>计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<p>计数加 5: {{ countPlus(5) }}</p>
<button @click="increment">增加</button>
<button @click="fetchData">获取数据</button>
</div>
</template>
<script>
import { computed } from 'vue'
import { useCounterStore } from '../stores/counter'
export default {
setup() {
const counterStore = useCounterStore()
// 直接访问 state
const count = computed(() => counterStore.count)
// 访问 getter
const doubleCount = computed(() => counterStore.doubleCount)
// 带参数的 getter
const countPlus = (amount) => counterStore.countPlus(amount)
// 调用 action
const increment = () => counterStore.increment()
const fetchData = () => counterStore.fetchData()
return {
count,
doubleCount,
countPlus,
increment,
fetchData
}
}
}
</script>修改 state 的最佳实践
// 错误:直接修改 state
counterStore.count++
// 正确:通过 action 修改
counterStore.increment()
// 或者使用 $patch 批量修改
counterStore.$patch({
count: counterStore.count + 1,
name: '新的计数器'
})
// 使用函数形式(更高效)
counterStore.$patch((state) => {
state.count++
state.name = '新的计数器'
})七、实用技巧与最佳实践
7.1 组合式函数 (Composables)
创建可复用的逻辑
// composables/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 }
}使用组合式函数
<template>
<div>
<p>X: {{ x }}</p>
<p>Y: {{ y }}</p>
</div>
</template>
<script>
import { useMouse } from '../composables/useMouse'
export default {
setup() {
const { x, y } = useMouse()
return {
x,
y
}
}
}
</script>7.2 模板引用 (Template Refs)
获取 DOM 元素
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">聚焦输入框</button>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建模板引用
const inputRef = ref(null)
function focusInput() {
// 通过 .value 访问 DOM 元素
inputRef.value.focus()
}
onMounted(() => {
// 组件挂载后可以访问
console.log(inputRef.value) // input 元素
})
return {
inputRef,
focusInput
}
}
}
</script>获取子组件实例
<!-- ParentComponent.vue -->
<template>
<div>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
setup() {
const childRef = ref(null)
function callChildMethod() {
// 调用子组件的方法
childRef.value.childMethod()
}
return {
childRef,
callChildMethod
}
}
}
</script><!-- ChildComponent.vue -->
<template>
<div>子组件</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
function childMethod() {
console.log('子组件方法被调用')
}
return {
childMethod
}
}
})
</script>7.3 性能优化技巧
v-memo:缓存渲染结果
<template>
<div>
<!-- 仅当 items 和 selectedId 发生变化时重新渲染 -->
<div v-for="item in items" :key="item.id" v-memo="[item.id === selectedId]">
{{ item.name }}
</div>
</div>
</template>组件懒加载
// 使用 defineAsyncComponent 懒加载组件
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
export default {
components: {
AsyncComponent
}
}Suspense 组件:处理异步依赖
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
}
</script>