Vue 3 知识点总结
最后更新:2026-07-29
目录
1. Vue 3 核心变化概览
1.1 与 Vue 2 的主要区别
| API 风格 | Options API | Options API + Composition API |
| 响应式原理 | Object.defineProperty | Proxy |
| 多根节点 | 不支持(需唯一根节点) | 支持 Fragment |
| 全局 API | Vue.xxx 挂载 | createApp() 应用实例 |
| 状态管理 | Vuex | Pinia(官方推荐) |
| 路由 | Vue Router 3 | Vue Router 4 |
| TypeScript | 支持较弱 | 原生 TS 编写,一等支持 |
| 包体积 | ~23KB | ~10KB(tree-shaking) |
| 渲染性能 | 基线 | 约 1.3~2 倍提升 |
| v-model | 仅一个 | 多个 v-model |
| 过渡类名 | v-enter | v-enter-from |
| 过滤器 | 支持 | 已移除 |
| $on/$off/$once | 支持 | 已移除(用 mitt 等替代) |
| 函数式组件 | 支持 | 仅保留函数式写法,性能优势消失 |
1.2 创建应用
// Vue 3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(router)
app.use(pinia)
app.mount('#app')
1.3 全局 API 变更
// Vue 2: Vue.component / Vue.directive / Vue.mixin
// Vue 3: 挂在 app 实例上
app.component('MyComp', MyComp)
app.directive('focus', { mounted(el) { el.focus() } })
app.mixin({ /* … */ })
app.config.globalProperties.$http = axios
app.config.errorHandler = (err) => { /* … */ }
2. 组合式 API(Composition API)
2.1 setup() 函数
<script setup>
// <script setup> 是语法糖,无需 return,顶层变量/函数自动暴露给模板
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('mounted')
})
</script>
普通 setup() 写法:
export default {
setup(props, { emit, expose, slots, attrs }) {
const count = ref(0)
// 必须 return 暴露给模板
return { count }
}
}
2.2 组合式函数(Composables)
将可复用逻辑抽取为函数,以 use 开头命名:
// useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value—
const reset = () => (count.value = initial)
return { count, increment, decrement, reset }
}
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter(10)
</script>
常用组合式函数示例:
// 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.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// useFetch.js – 数据请求
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isPending = ref(true)
watchEffect(async () => {
isPending.value = true
data.value = null
error.value = null
try {
const res = await fetch(toValue(url))
if (!res.ok) throw new Error(res.statusText)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
isPending.value = false
}
})
return { data, error, isPending }
}
2.3 provide / inject(依赖注入)
// 父组件
import { provide, ref, readonly } from 'vue'
const count = ref(0)
provide('count', readonly(count)) // 只读注入
provide('increment', () => count.value++) // 注入方法
// 也可用 Symbol / InjectionKey 做类型安全
export const CountKey = Symbol('count')
provide(CountKey, count)
// 子组件(任意深度)
import { inject } from 'vue'
const count = inject('count', 0) // 第二个参数为默认值
3. 响应式系统
3.1 核心 API
ref()
import { ref } from 'vue'
const count = ref(0) // 基本类型
count.value++ // 通过 .value 访问/修改
const obj = ref({ a: 1 }) // 对象也可以,内部用 reactive 包装
obj.value.a = 2
// 模板中自动解包,无需 .value
// <p>{{ count }}</p>
reactive()
import { reactive } from 'vue'
const state = reactive({
count: 0,
nested: { deep: true }
})
state.count++ // 直接修改,无需 .value
state.nested.deep // 深层也是响应式的
// ⚠️ 不能解构,否则丢失响应性
// const { count } = state // ❌ count 不再是响应式
ref vs reactive 选择
| 基本类型(number/string/boolean) | ref |
| 需要整体替换的对象 | ref |
| 表单等固定结构的对象 | reactive |
| 通用 / 不确定 | ref(更安全) |
3.2 解构保持响应性
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({ name: 'Vue', version: 3 })
// toRefs:整体解构
const { name, version } = toRefs(state) // 每个都是 ref
// toRef:单个属性
const nameRef = toRef(state, 'name')
3.3 只读与浅层
import { readonly, shallowRef, shallowReactive, shallowReadonly, triggerRef } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// copy.count++ // ❌ 警告,不可修改
const shallow = shallowRef({ nested: { count: 0 } })
// shallow.value.nested.count++ // ❌ 不触发更新
shallow.value = { nested: { count: 1 } } // ✅ 替换整个 .value 才触发
// 手动触发 shallowRef 更新
triggerRef(shallow)
3.4 响应式原理(Proxy)
reactive(obj)
→ new Proxy(obj, {
get(target, key, receiver) {
track(target, key) // 收集依赖
const res = Reflect.get(target, key, receiver)
return isObject(res) ? reactive(res) : res // 惰性深层代理
},
set(target, key, value, receiver) {
const oldVal = target[key]
const res = Reflect.set(target, key, value, receiver)
if (oldVal !== value) trigger(target, key) // 触发更新
return res
},
deleteProperty(target, key) { … }
})
优势:
- 可检测属性新增/删除
- 可检测数组索引/length 变化
- 惰性深层代理(访问时才代理子对象),性能更好
3.5 响应式工具函数
import { isRef, isReactive, isReadonly, isProxy, toRaw, markRaw, customRef } from 'vue'
isRef(ref(0)) // true
isReactive(reactive({})) // true
isProxy(reactive({})) // true
// toRaw:获取原始对象(跳过代理)
const raw = toRaw(state)
// markRaw:标记对象永不转为响应式(适合第三方库实例)
const chart = markRaw(new ECharts())
// customRef:自定义 ref(如防抖)
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}))
}
4. 生命周期钩子
4.1 选项式 vs 组合式对照
| beforeCreate | setup() 本身 | setup 在此阶段执行 |
| created | setup() 本身 | |
| beforeMount | onBeforeMount | |
| mounted | onMounted | DOM 已挂载 |
| beforeUpdate | onBeforeUpdate | |
| updated | onUpdated | |
| beforeUnmount | onBeforeUnmount | |
| unmounted | onUnmounted | 清理副作用 |
| errorCaptured | onErrorCaptured | |
| renderTracked | onRenderTracked | 调试:依赖追踪 |
| renderTriggered | onRenderTriggered | 调试:触发更新 |
| activated | onActivated | KeepAlive 激活 |
| deactivated | onDeactivated | KeepAlive 停用 |
| serverPrefetch | onServerPrefetch | SSR 专用 |
4.2 使用示例
<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
console.log('DOM ready')
window.addEventListener('resize', handler)
})
onUnmounted(() => {
window.removeEventListener('resize', handler)
})
// 可多次调用,按注册顺序执行
onMounted(() => { /* 第二个回调 */ })
</script>
4.3 生命周期图示
setup()
│
├─ onBeforeMount
│ │
│ 挂载 DOM
│ │
├─ onMounted ◄── 可访问 DOM / 发起请求
│ │
│ 数据变化 → 重新渲染
│ │
├─ onBeforeUpdate
├─ onUpdated
│ │
│ 卸载组件
│ │
├─ onBeforeUnmount ◄── 清理定时器/事件
└─ onUnmounted
5. 计算属性与侦听器
5.1 computed
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写计算属性
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})
特点: 基于依赖缓存,依赖不变则不重新计算。
5.2 watch
import { ref, watch, reactive } from 'vue'
const count = ref(0)
const state = reactive({ name: '', age: 0 })
// 1. 侦听 ref
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`)
})
// 2. 侦听 reactive 对象的某个属性(必须用 getter)
watch(
() => state.name,
(newName) => console.log(newName)
)
// 3. 侦听多个源
watch([count, () => state.name], ([newCount, newName]) => {
// …
})
// 4. 深度侦听
watch(state, (newState) => {
// 任何嵌套属性变化都触发
}, { deep: true })
// 5. 立即执行
watch(count, (val) => {
// 立即执行一次
}, { immediate: true })
// 6. 回调刷新时机
watch(count, cb, { flush: 'post' }) // DOM 更新后执行
watch(count, cb, { flush: 'sync' }) // 同步执行(谨慎使用)
// 7. 一次性侦听(Vue 3.4+)
watch(count, cb, { once: true })
// 8. 停止侦听
const stop = watch(count, cb)
stop() // 手动停止
5.3 watchEffect
import { watchEffect } from 'vue'
// 自动追踪回调中使用的所有响应式依赖
const stop = watchEffect((onCleanup) => {
console.log(count.value) // 自动依赖 count
// 清理函数:下次执行前或组件卸载时调用
const timer = setInterval(() => {}, 1000)
onCleanup(() => clearInterval(timer))
})
// 停止
stop()
5.4 watch vs watchEffect
| 指定侦听源 | ✅ 显式指定 | ❌ 自动追踪 |
| 访问旧值 | ✅ (new, old) | ❌ |
| 惰性执行 | ✅ 默认惰性 | ❌ 立即执行 |
| 深度选项 | ✅ { deep: true } | 自动深度追踪 |
6. 组件系统
6.1 组件定义
<!– 单文件组件 SFC –>
<script setup lang="ts">
// 组件逻辑
</script>
<template>
<!– 模板 –>
</template>
<style scoped>
/* 局部样式 */
</style>
6.2 Props
<script setup lang="ts">
// 方式一:defineProps 宏(编译时,无需 import)
const props = defineProps<{
title: string
count?: number
}>()
// 带默认值
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0
})
// 运行时声明
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
list: { type: Array as PropType<string[]>, default: () => [] }
})
</script>
Props 注意事项:
- Props 是只读的,不要直接修改
- 对象/数组类型的 default 必须用工厂函数
- 未声明的 attribute 会落入 $attrs
6.3 Emits
<script setup lang="ts">
const emit = defineEmits<{
change: [value: string]
update: [id: number, data: object]
}>()
// 运行时声明
const emit = defineEmits(['change', 'update'])
// 触发
emit('change', 'hello')
emit('update', 1, { name: 'test' })
</script>
6.4 v-model(组件双向绑定)
<!– 父组件:多个 v-model –>
<UserForm
v-model:firstName="first"
v-model:lastName="last"
/>
<!– UserForm.vue –>
<script setup>
const firstName = defineModel('firstName') // Vue 3.4+ 语法糖
const lastName = defineModel('lastName')
// 等价于:
// const props = defineProps(['firstName', 'lastName'])
// const emit = defineEmits(['update:firstName', 'update:lastName'])
</script>
修饰符:
<UserForm v-model:firstName.trim="first" />
<!– 子组件中 –>
<script setup>
const [model, modifiers] = defineModel('firstName')
if (modifiers.trim) { /* … */ }
</script>
6.5 Slots 插槽
<!– 父组件 –>
<Card>
<template #header>标题</template>
<template #default="{ item }">
{{ item.name }}
</template>
<template #footer>底部</template>
</Card>
<!– Card.vue –>
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot :item="currentItem" /></main>
<footer><slot name="footer">默认底部</slot></footer>
</div>
</template>
// 在 setup 中访问插槽
import { useSlots } from 'vue'
const slots = useSlots()
// slots.default?.()
// slots.header?.()
6.6 expose / 组件实例
<!– 子组件 –>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const reset = () => (count.value = 0)
// 只暴露 reset,不暴露 count
defineExpose({ reset })
</script>
<!– 父组件 –>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
childRef.value.reset() // ✅
// childRef.value.count // ❌ 未暴露
})
</script>
<template>
<Child ref="childRef" />
</template>
6.7 attrs 透传
<!– 自动继承:未声明的 props/emits 自动绑定到根元素 –>
<!– 禁用继承 –>
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<input v-bind="$attrs" />
</template>
6.8 动态组件
<script setup>
import { shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
const current = shallowRef(TabA)
</script>
<template>
<component :is="current" />
</template>
7. 模板语法与指令
7.1 插值与绑定
<!– 文本插值 –>
<p>{{ message }}</p>
<!– 原始 HTML –>
<div v-html="rawHtml"></div>
<!– 属性绑定 –>
<img :src="imageUrl" :alt="title" />
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[activeClass, errorClass]"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!– 动态参数 –>
<a :[attrName]="url">Link</a>
<button @[eventName]="handler">Click</button>
7.2 条件渲染
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
<!– v-show:切换 display,频繁切换时性能更好 –>
<div v-show="isVisible">Toggle me</div>
<!– template 上使用 v-if(不渲染额外元素) –>
<template v-if="showGroup">
<h1>Title</h1>
<p>Content</p>
</template>
7.3 列表渲染
<li v-for="(item, index) in items" :key="item.id">
{{ index }} – {{ item.name }}
</li>
<!– 对象遍历 –>
<div v-for="(value, key, index) in obj" :key="key">
{{ key }}: {{ value }}
</div>
<!– 范围 –>
<span v-for="n in 10" :key="n">{{ n }}</span>
⚠️ key 的重要性: 必须用唯一标识(如 id),不要用 index。
7.4 事件处理
<button @click="handler">Click</button>
<button @click="handler($event, 'arg')">With args</button>
<!– 修饰符 –>
<form @submit.prevent="onSubmit"> <!– 阻止默认 –>
<a @click.stop="doThis"> <!– 阻止冒泡 –>
<div @click.self="onlySelf"> <!– 仅自身触发 –>
<input @keyup.enter="submit"> <!– 按键修饰符 –>
<button @click.once="doOnce"> <!– 仅触发一次 –>
<div @scroll.passive="onScroll"> <!– passive 监听 –>
7.5 表单绑定
<input v-model="text" />
<textarea v-model="desc"></textarea>
<input type="checkbox" v-model="checked" />
<input type="checkbox" v-model="picked" value="a" />
<input type="radio" v-model="picked" value="b" />
<select v-model="selected">
<option value="1">One</option>
</select>
<!– 修饰符 –>
<input v-model.trim="msg" /> <!– 自动 trim –>
<input v-model.number="age" /> <!– 转 number –>
<input v-model.lazy="msg" /> <!– change 而非 input 事件 –>
8. 内置组件
8.1 Transition
<template>
<Transition name="fade" mode="out-in">
<p v-if="show">Hello</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
过渡类名(Vue 3):
- v-enter-from → v-enter-active → v-enter-to
- v-leave-from → v-leave-active → v-leave-to
8.2 TransitionGroup
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>
<style>
.list-enter-active, .list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.5s ease;
}
</style>
8.3 KeepAlive
<KeepAlive :include="['TabA', 'TabB']" :max="10">
<component :is="currentTab" />
</KeepAlive>
<!– 配合生命周期 –>
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => { /* 从缓存恢复 */ })
onDeactivated(() => { /* 进入缓存 */ })
</script>
9. 路由(Vue Router 4)
9.1 基本配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue') // 懒加载
},
{
path: '/user/:id',
name: 'User',
component: () => import('../views/User.vue'),
props: true, // 将 params 作为 props 传入
children: [
{ path: 'profile', component: () => import('../views/Profile.vue') }
]
},
{
path: '/:pathMatch(.*)*', // 404 兜底
name: 'NotFound',
component: () => import('../views/404.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
export default router
9.2 组合式 API 中使用
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 编程式导航
router.push('/user/1')
router.push({ name: 'User', params: { id: 1 } })
router.replace('/login')
router.back()
router.go(-2)
// 当前路由信息
console.log(route.params.id)
console.log(route.query.search)
console.log(route.hash)
</script>
9.3 导航守卫
// 全局前置守卫
router.beforeEach((to, from) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
// 全局后置钩子
router.afterEach((to, from) => {
document.title = to.meta.title || 'App'
})
// 路由独享守卫
{
path: '/admin',
component: Admin,
beforeEnter: (to, from) => {
if (!isAdmin()) return false
}
}
<!– 组件内守卫 –>
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return window.confirm('确定离开?未保存的更改将丢失。')
}
})
onBeforeRouteUpdate((to, from) => {
// 同组件复用,参数变化时(如 /user/1 → /user/2)
fetchData(to.params.id)
})
</script>
9.4 路由元信息
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
title: '控制台',
roles: ['admin', 'editor']
}
}
10. 状态管理(Pinia)
10.1 基本使用
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 组合式写法(推荐)
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('Vue')
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
async function fetchCount() {
const res = await api.getCount()
count.value = res.data
}
return { count, name, doubleCount, increment, fetchCount }
})
// 选项式写法
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Vue' }),
getters: {
doubleCount: (state) => state.count * 2,
doublePlusOne() { return this.doubleCount + 1 }
},
actions: {
increment() { this.count++ },
async fetchCount() {
this.count = await api.getCount()
}
}
})
10.2 在组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// ⚠️ 解构会丢失响应性,用 storeToRefs
const { count, doubleCount } = storeToRefs(store)
// actions 可以直接解构
const { increment } = store
// 修改 state
store.count++
store.$patch({ count: 10, name: 'Pinia' })
store.$patch((state) => {
state.count++
state.name = 'batch'
})
store.$reset() // 重置(仅选项式写法支持)
</script>
10.3 Store 组合
// stores/user.js
export const useUserStore = defineStore('user', () => {
const token = ref('')
const isLoggedIn = computed(() => !!token.value)
return { token, isLoggedIn }
})
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
const user = useUserStore() // 组合其他 store
const items = ref([])
const canCheckout = computed(() => user.isLoggedIn && items.value.length > 0)
return { items, canCheckout }
})
10.4 插件与持久化
// pinia 插件
function piniaLogger({ store }) {
store.$subscribe((mutation, state) => {
console.log(`[${store.$id}] ${mutation.type}`, state)
})
}
const pinia = createPinia()
pinia.use(piniaLogger)
// 持久化(pinia-plugin-persistedstate)
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)
// store 中启用
export const useUserStore = defineStore('user', () => {
// …
}, {
persist: true // 或 { key: 'user', storage: localStorage }
})
11. TypeScript 集成
11.1 项目配置
npm create vue@latest # 选择 TypeScript
// tsconfig.json 关键配置
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"paths": { "@/*": ["./src/*"] }
}
}
11.2 组件中的 TS
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { Ref } from 'vue'
// 自动推断
const count = ref(0) // Ref<number>
// 显式泛型
const list = ref<string[]>([])
const user = ref<{ name: string; age: number } | null>(null)
// Props 类型
interface Props {
title: string
items?: string[]
}
const props = withDefaults(defineProps<Props>(), {
items: () => []
})
// Emits 类型
const emit = defineEmits<{
change: [value: string]
delete: [id: number]
}>()
// Ref 类型
const inputRef = ref<HTMLInputElement | null>(null)
// 组件 ref 类型
import Child from './Child.vue'
const childRef = ref<InstanceType<typeof Child> | null>(null)
</script>
11.3 注入类型安全
import type { InjectionKey, Ref } from 'vue'
export const CountKey: InjectionKey<Ref<number>> = Symbol('count')
// provide
provide(CountKey, ref(0))
// inject – 自动推断为 Ref<number>
const count = inject(CountKey)!
11.4 模板 ref 类型
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM 元素
const el = ref<HTMLDivElement | null>(null)
// 组件实例
import MyComp from './MyComp.vue'
const comp = ref<InstanceType<typeof MyComp> | null>(null)
onMounted(() => {
el.value?.focus()
comp.value?.reset()
})
</script>
<template>
<div ref="el" tabindex="0">Focusable</div>
<MyComp ref="comp" />
</template>
12. 性能优化
12.1 编译层优化
- Tree-shaking:未使用的 API 不会打包
- 静态提升(hoistStatic):静态节点提升到渲染函数外
- Patch Flags:编译时标记动态绑定类型,运行时精准 diff
- Block Tree:将模板分块,只追踪动态节点
- 缓存事件处理函数:cacheHandlers
12.2 运行时优化
<script setup>
import { shallowRef, vModelText } from 'vue'
// 1. 大列表用 shallowRef / shallowReactive
const bigList = shallowRef(hugeArray)
// 2. v-once:只渲染一次
// <div v-once>{{ staticContent }}</div>
// 3. v-memo:条件记忆化
// <div v-memo="[item.id === selected]">…</div>
// 4. 虚拟列表(大量数据)
// 使用 vue-virtual-scroller / @tanstack/vue-virtual
</script>
12.3 组件级优化
// 1. 异步组件 / 代码分割
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./HeavyChart.vue')
)
// 2. KeepAlive 缓存
// <KeepAlive :max="5">
// <component :is="currentView" />
// </KeepAlive>
// 3. 避免不必要的响应式
import { markRaw } from 'vue'
const chartInstance = markRaw(new ECharts()) // 不需要响应式
// 4. 函数式组件(纯展示)
const FuncComp = (props) => h('div', props.msg)
12.4 列表优化
<!– ✅ 正确:唯一 key –>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
<!– ❌ 错误:用 index –>
<li v-for="(item, i) in list" :key="i">{{ item.name }}</li>
<!– 大列表:分页 / 虚拟滚动 –>
<VirtualList :items="items" :item-height="50" v-slot="{ item }">
<div>{{ item.name }}</div>
</VirtualList>
12.5 其他技巧
// 防抖/节流搜索
import { useDebouncedRef } from './useDebouncedRef'
const search = useDebouncedRef('', 300)
watch(search, (val) => fetchResults(val))
// 图片懒加载
// <img v-lazy="imageUrl" /> (自定义指令或 vue-lazyload)
// 路由懒加载
component: () => import('./views/About.vue')
// 第三方库按需引入
// import { debounce } from 'lodash-es' // ✅
// import _ from 'lodash' // ❌
13. 编译优化
13.1 Patch Flags 详解
// 编译前
// <div><span>static</span><span>{{ dynamic }}</span></div>
// 编译后(简化)
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "static", –1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}
常见 PatchFlag:
| TEXT | 1 | 动态文本 |
| CLASS | 2 | 动态 class |
| STYLE | 4 | 动态 style |
| PROPS | 8 | 动态非 class/style props |
| FULL_PROPS | 16 | 动态 key 不确定的 props |
| NEED_HYDRATION | 32 | SSR 需要水合 |
| STABLE_FRAGMENT | 64 | 子节点顺序不变 |
| KEYED_FRAGMENT | 128 | 有 key 的 fragment |
| UNKEYED_FRAGMENT | 256 | 无 key 的 fragment |
| HOISTED | -1 | 静态提升 |
| BAIL | -2 | 退出优化模式 |
13.2 Block Tree
<template>
<div> <!– Block root –>
<p>static</p> <!– 不追踪 –>
<p>{{ a }}</p> <!– 追踪 –>
<p v-if="ok">{{ b }}</p> <!– 条件块 –>
<p>{{ c }}</p> <!– 追踪 –>
</div>
</template>
运行时 diff 只比较 Block 收集的动态节点数组,跳过所有静态内容。
14. Teleport / Suspense / 异步组件
14.1 Teleport
将内容渲染到 DOM 树的其他位置:
<template>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<p>弹窗内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<!– 禁用 teleport –>
<Teleport to="body" :disabled="isMobile">
<div class="tooltip">…</div>
</Teleport>
14.2 Suspense(实验性)
<template>
<Suspense>
<!– 默认插槽:异步内容 –>
<template #default>
<AsyncDashboard /> <!– setup() 中有 await 的组件 –>
</template>
<!– 加载状态 –>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<!– AsyncDashboard.vue –>
<script setup>
// 顶层 await → 组件变为异步依赖
const data = await fetchDashboardData()
</script>
Suspense 事件:
<Suspense
@resolve="onResolve"
@pending="onPending"
@fallback="onFallback"
>
14.3 异步组件
import { defineAsyncComponent } from 'vue'
// 简单用法
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'))
// 完整配置
const AsyncCompWithOptions = defineAsyncComponent({
loader: () => import('./Comp.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 显示 loading 前的延迟
timeout: 10000, // 超时
suspensible: false, // 是否被 Suspense 接管
onError(error, retry, fail, attempts) {
if (attempts <= 3) retry()
else fail()
}
})
15. 自定义指令
15.1 定义
// Vue 3 钩子名称与生命周期对齐
const myDirective = {
created(el, binding, vnode) {}, // 元素创建后
beforeMount(el, binding, vnode) {}, // 挂载前
mounted(el, binding, vnode) {}, // 挂载后
beforeUpdate(el, binding, vnode, prevVnode) {},
updated(el, binding, vnode, prevVnode) {},
beforeUnmount(el, binding, vnode) {},
unmounted(el, binding, vnode) {}
}
15.2 使用
<script setup>
// 局部注册(script setup 中以 v 开头的变量自动注册)
const vFocus = {
mounted: (el) => el.focus()
}
const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value || 'yellow'
},
updated(el, binding) {
el.style.backgroundColor = binding.value || 'yellow'
}
}
</script>
<template>
<input v-focus />
<p v-highlight="'lightblue'">高亮文本</p>
<p v-highlight>默认黄色</p>
</template>
// 全局注册
app.directive('focus', {
mounted(el) { el.focus() }
})
// 简写(mounted + updated 相同逻辑)
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
15.3 binding 对象
{
value: '绑定值', // v-xxx:value
oldValue: '旧值', // 仅 updated
arg: '参数', // v-xxx:arg
modifiers: { mod: true }, // v-xxx.mod
instance: null, // 组件实例
dir: { /* 指令定义 */ }
}
16. 插件系统
16.1 编写插件
// myPlugin.js
export default {
install(app, options) {
// 全局组件
app.component('MyIcon', MyIcon)
// 全局指令
app.directive('tooltip', tooltipDirective)
// 全局属性
app.config.globalProperties.$format = (val) => { /* … */ }
// provide 注入
app.provide('pluginOptions', options)
// 全局 mixin(谨慎使用)
app.mixin({
created() { /* … */ }
})
}
}
16.2 使用插件
import myPlugin from './myPlugin'
const app = createApp(App)
app.use(myPlugin, { theme: 'dark' })
app.mount('#app')
17. 渲染函数与 JSX
17.1 h() 函数
import { h, ref } from 'vue'
// 基本用法
const vnode = h('div', { class: 'container' }, [
h('h1', 'Title'),
h('p', { style: 'color: red' }, 'Content')
])
// 组件
import MyComp from './MyComp.vue'
const compVnode = h(MyComp, {
title: 'Hello',
onChange: (val) => console.log(val)
}, {
default: () => 'Slot content',
header: () => h('h2', 'Header')
})
// 条件渲染
const render = () => {
return show.value
? h('div', 'Visible')
: h('span', 'Hidden')
}
// 列表渲染
const render = () => {
return h('ul', items.value.map(item =>
h('li', { key: item.id }, item.name)
))
}
17.2 JSX(需 @vitejs/plugin-vue-jsx)
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
return () => (
<div class="container">
<h1>Count: {count.value}</h1>
<button onClick={() => count.value++}>+1</button>
{count.value > 5 && <p>Big number!</p>}
<ul>
{items.value.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
})
17.3 渲染函数 vs 模板
| 常规 UI | 模板(可读性 + 编译优化) |
| 高度动态/递归组件 | 渲染函数 |
| 组件库/抽象组件 | 渲染函数 / JSX |
| 需要完整 JS 表达力 | JSX |
18. 服务端渲染(SSR)
18.1 基本流程
客户端请求
│
▼
服务器执行 Vue 组件 → 生成 HTML 字符串
│
▼
返回完整 HTML(首屏可见)
│
▼
客户端加载 JS → 水合(Hydration)→ 交互可用
18.2 核心 API
// 服务器端
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'
const app = createSSRApp(App)
const html = await renderToString(app)
// 客户端
import { createSSRApp } from 'vue'
const app = createSSRApp(App)
app.mount('#app') // 水合而非重新渲染
18.3 数据预取
<script setup>
import { useAsyncData } from '#app' // Nuxt 3
const { data, pending, error } = await useAsyncData('posts', () =>
$fetch('/api/posts')
)
</script>
18.4 流式渲染(Vue 3.5+)
import { renderToStream } from 'vue/server-renderer'
const stream = renderToStream(app)
stream.pipe(res) // Node.js response
18.5 Nuxt 3 框架
npx nuxi@latest init my-app
nuxt.config.ts # 配置
pages/ # 文件系统路由
components/ # 自动导入组件
composables/ # 自动导入组合式函数
server/api/ # API 路由
middleware/ # 路由中间件
plugins/ # 插件
19. 测试
19.1 单元测试(Vitest)
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // 或 happy-dom
globals: true
}
})
// Counter.test.ts
import { describe, it, expect } from 'vitest'
import { ref, computed, nextTick } from 'vue'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('increments', () => {
const { count, increment } = useCounter(0)
increment()
expect(count.value).toBe(1)
})
it('computes double', async () => {
const { count, double } = useCounter(5)
expect(double.value).toBe(10)
count.value = 10
await nextTick()
expect(double.value).toBe(20)
})
})
19.2 组件测试(Vue Test Utils)
// npm i -D @vue/test-utils
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('renders count', () => {
const wrapper = mount(Counter, {
props: { initial: 5 }
})
expect(wrapper.text()).toContain('5')
})
it('increments on click', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.find('.count').text()).toBe('1')
})
it('emits change event', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('change')).toHaveLength(1)
expect(wrapper.emitted('change')[0]).toEqual([1])
})
it('renders slot', () => {
const wrapper = mount(Counter, {
slots: { default: '<span class="custom">Slot</span>' }
})
expect(wrapper.find('.custom').exists()).toBe(true)
})
})
19.3 E2E 测试(Playwright / Cypress)
// playwright
import { test, expect } from '@playwright/test'
test('counter interaction', async ({ page }) => {
await page.goto('/')
await page.click('button.increment')
await expect(page.locator('.count')).toHaveText('1')
})
20. 常见设计模式与最佳实践
20.1 组件设计原则
✅ 单一职责:一个组件只做一件事
✅ Props 向下,Events 向上(单向数据流)
✅ 组合优于继承
✅ 合理拆分:展示组件 vs 容器组件
✅ 命名:多单词 PascalCase(MyComponent)
✅ 避免 v-if 和 v-for 同时使用
✅ 大列表必须用 key,且不用 index
20.2 常见模式
受控组件模式
<!– 父组件完全控制 –>
<Input :value="text" @update:value="text = $event" />
<!– 或 –>
<Input v-model="text" />
渲染代理 / 无渲染组件
<!– RenderlessList.vue –>
<script setup>
defineProps({ items: Array })
</script>
<template>
<slot :items="items" :isEmpty="!items.length" />
</template>
<!– 使用 –>
<RenderlessList :items="todos" v-slot="{ items, isEmpty }">
<p v-if="isEmpty">暂无数据</p>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
</RenderlessList>
递归组件
<!– TreeNode.vue –>
<script setup>
defineProps({ node: Object })
</script>
<template>
<li>
{{ node.label }}
<ul v-if="node.children">
<TreeNode
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</ul>
</li>
</template>
20.3 错误处理
<!– ErrorBoundary 组件 –>
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err
return false // 阻止继续向上传播
})
</script>
<template>
<div v-if="error" class="error">
<p>出错了:{{ error.message }}</p>
<button @click="error = null">重试</button>
</div>
<slot v-else />
</template>
20.4 项目结构推荐
src/
├── api/ # API 请求封装
│ ├── index.ts # axios 实例
│ ├── user.ts
│ └── post.ts
├── assets/ # 静态资源
├── components/ # 通用组件
│ ├── ui/ # 基础 UI 组件
│ └── business/ # 业务组件
├── composables/ # 组合式函数
│ ├── useAuth.ts
│ ├── useFetch.ts
│ └── usePagination.ts
├── directives/ # 自定义指令
├── layouts/ # 布局组件
├── pages/ (views/) # 页面组件
├── router/ # 路由配置
│ └── index.ts
├── stores/ # Pinia stores
│ ├── user.ts
│ └── app.ts
├── styles/ # 全局样式
├── types/ # TS 类型定义
├── utils/ # 工具函数
├── App.vue
└── main.ts
20.5 常用工具库生态
| 构建工具 | Vite(官方)/ Nuxt |
| 路由 | Vue Router 4 |
| 状态管理 | Pinia |
| HTTP | axios / ky / ofetch |
| 工具函数 | VueUse(强烈推荐) |
| UI 组件库 | Element Plus / Ant Design Vue / Naive UI / Vuetify 3 / PrimeVue |
| CSS | Tailwind CSS / UnoCSS |
| 图标 | @iconify/vue / lucide-vue-next |
| 表单验证 | vee-validate + zod / valibot |
| 图表 | ECharts / Chart.js |
| 动画 | GSAP / @vueuse/motion |
| 国际化 | vue-i18n |
| 测试 | Vitest + Vue Test Utils + Playwright |
| 代码规范 | ESLint + Prettier + husky |
20.6 VueUse 常用函数
import {
useStorage, // 响应式 localStorage
useDark, // 暗黑模式
useToggle, // 切换布尔值
useMouse, // 鼠标位置
useWindowSize, // 窗口尺寸
useIntersectionObserver, // 交叉观察
useEventListener, // 自动清理事件监听
useDebounceFn, // 防抖函数
useThrottleFn, // 节流函数
useClipboard, // 剪贴板
useMediaQuery, // 媒体查询
useFetch, // 数据请求
useVModel, // 组件 v-model 辅助
} from '@vueuse/core'
附录:Vue 3.4 / 3.5+ 新特性速览
Vue 3.4
- defineModel() 稳定版
- watch 支持 { once: true }
- 改进的响应式系统(更精确的依赖追踪)
- 解析器性能提升 2 倍
Vue 3.5
- 响应式系统重写:内存占用降低 56%,使用双向链表替代 Set
- useTemplateRef() 替代 ref + 模板 ref 字符串
- useId() 生成 SSR 安全的唯一 ID
- onWatcherCleanup() 清理函数
- Lazy hydration(异步组件按需水合)
- app.onUnmount() 清理应用级副作用
Vue 3.6+(Vapor Mode,开发中)
- Vapor Mode:无虚拟 DOM 的编译模式
- 直接编译为命令式 DOM 操作
- 性能接近原生 JS,内存占用大幅降低
- 可与现有虚拟 DOM 组件混用
快速参考卡片
// 响应式
ref(0) // 基本类型
reactive({}) // 对象
computed(() => {}) // 计算属性
watch(source, cb, opts) // 侦听器
watchEffect(fn) // 自动追踪
// 生命周期
onMounted / onUnmounted / onUpdated / onActivated
// 组件通信
defineProps / defineEmits / defineModel / defineExpose
provide / inject
useSlots / useAttrs
// 工具
toRef / toRefs / toRaw / markRaw
readonly / shallowRef / shallowReactive
nextTick / defineAsyncComponent
// 路由
useRouter / useRoute
router.push / replace / back / go
// Pinia
defineStore / storeToRefs / $patch / $reset
📚 参考资源:
- Vue 3 官方文档
- Vue Router 文档
- Pinia 文档
- VueUse 文档
- Vue 3 RFC
网硕互联帮助中心



评论前必须登录!
注册