Vue 3 深度笔记
本文聚焦 Vue 3,以组合式 API 和编译优化为主线,覆盖响应式原理、虚拟 DOM、路由、状态管理等核心领域。
1. 响应式原理
1.1 Vue 2:Object.defineProperty 的局限
Vue 2 通过 Object.defineProperty 递归遍历 data 对象,为每个属性定义 getter/setter。
核心局限:
- 无法检测属性的新增和删除 — 必须用
Vue.set()/Vue.delete() - 无法检测数组索引赋值和长度变更 — 重写了 7 个数组方法(push/pop/shift/unshift/splice/sort/reverse),但直接
arr[index] = val无效 - 递归深度大 — 初始化时一次性递归全部属性,对象嵌套深时性能开销大
- 无法代理 Map/Set 等新数据结构
1.2 Vue 3:Proxy
const handler = {
get(target, key, receiver) {
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
}
}
Proxy 优势:
- 拦截整个对象而非属性,新增/删除属性自动响应
- 数组无需特殊 hack,下标赋值、length 变更均可拦截
- 拦截操作丰富(has/deleteProperty/ownKeys 等 13 种)
- 懒递归 — 只有 get 到嵌套对象时才将其包装为响应式
1.3 ref vs reactive
| 特性 | ref |
reactive |
|---|---|---|
| 数据类型 | 任意 | 仅对象 |
| 访问方式 | .value(template 中自动 unwrap) |
直接访问 |
| 原理 | 使用 reactive 包装 .value,本质是 { value: reactive(obj) } |
Proxy 直接代理 |
| 解构 | 解构后仍响应(自动 unwrap) | 解构后丢失响应(需 toRefs) |
| 追踪 | 通过 RefImpl 类管理依赖 | 通过 Proxy 捕获 |
核心原则:
- 涉及基础类型或需要重新赋值时用
ref - 复杂对象结构用
reactive - 如果团队风格统一,全用
ref也可以(Vue 官方推荐场景偏向 ref)
1.4 shallowRef / shallowReactive
只对顶层属性做响应式处理,深层次修改不触发更新。适用于大对象或第三方状态管理。
const state = shallowRef({ a: { b: 1 } })
state.value.a.b = 2 // 不触发响应
state.value = { a: { b: 2 } } // 触发响应(顶层引用变化)
1.5 toRaw / markRaw
import { toRaw, markRaw } from 'vue'
const original = { foo: 1 }
const reactiveObj = reactive(original)
toRaw(reactiveObj) === original // true
// markRaw 跳过代理,适合第三方实例或不可变数据
const rawObj = markRaw({ foo: 1 })
reactive(rawObj) // 仍为原始对象
1.6 readonly
将响应式对象变为只读代理(递归),内部调用 reactive 包装后替换 setter。
const state = reactive({ count: 0 })
const readOnlyState = readonly(state)
readOnlyState.count++ // warn: Set operation on key "count" failed
1.7 effectScope
用于管理多个 effect(watch/computed/watchEffect)的生命周期,适用于组件库、插件或需批量停止响应的场景。
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => state.count * 2)
watch(doubled, (val) => console.log(val))
})
// 批量停止
scope.stop()
替代 Vue 2 中 vm.$destroy 的碎片化清理。
2. 组合式 API
2.1 setup
setup() 在组件创建前执行,接收 props 和 context,返回的值暴露给模板。
// <script setup> 语法(推荐)
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<script setup> 的优势:更少的样板代码、更好的类型推断、顶层绑定自动暴露给模板。
2.2 ref / reactive
见上节。
2.3 computed(缓存原理)
computed 本质是一个 ComputedRefImpl 类,内部维护一个 dirty 标志(懒求值):
第一次读取 computed → dirty === true → 执行 getter → 缓存结果 → dirty = false
依赖变化 → trigger → dirty = true
再次读取 → dirty === true → 重新求值
- 只有依赖变化且被读取时才重新计算(惰性求值)
- 无依赖变化的重复读取直接返回缓存
- 可写 computed:传入
{ get, set }对象
const count = ref(1)
const doubled = computed(() => count.value * 2)
// 可写 computed
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})
2.4 watch / watchEffect
| 特性 | watch |
watchEffect |
|---|---|---|
| 执行时机 | 懒执行(监听变化后执行) | 立即执行一次 |
| 访问依赖 | 显式指定监听源 | 自动追踪函数内响应式依赖 |
| 旧值 | 提供 | 不提供 |
| 场景 | 需对比新旧值、复杂监听 | 只需执行副作用,无需旧值 |
// watch
watch(source, (newVal, oldVal) => {}, {
deep: true, // 深度监听(开销大)
immediate: true, // 立即执行
flush: 'post' // post(默认,DOM更新后) | pre(DOM更新前) | sync(同步)
})
// watchEffect
watchEffect(() => console.log(state.count), {
flush: 'post' // 推荐使用 'post' 避免 DOM 未更新问题
})
flush 选项:
pre(默认):在组件更新前执行,此时 DOM 尚未更新post:在组件更新后执行,此时 DOM 已更新sync:响应式依赖变化时同步执行,性能最差
2.5 生命周期钩子
onMounted(() => {}) // 挂载完成
onUpdated(() => {}) // 更新后
onUnmounted(() => {}) // 卸载前清理
onBeforeMount(() => {})
onBeforeUpdate(() => {})
onBeforeUnmount(() => {})
onErrorCaptured(() => {})
onActivated(() => {}) // 配合 KeepAlive
onDeactivated(() => {}) // 配合 KeepAlive
Vue 3 中 beforeDestroy → onBeforeUnmount,destroyed → onUnmounted。
2.6 provide / inject
// 祖先组件
provide('theme', ref('dark'))
provide('userService', new UserService())
// 后代组件
const theme = inject('theme', 'light') // 第二个参数为默认值
配合 readonly 防止子组件修改注入值:
provide('theme', readonly(ref('dark')))
3. 虚拟 DOM 与 Diff
3.1 VNode 结构
interface VNode {
__v_isVNode: true
type: string | Component | Fragment | Text | Static
props: Record<string, any> | null
children: VNode[] | string | null
patchFlag?: number // Vue 3 编译优化标志
dynamicProps?: string[] // 动态属性列表
staticCount?: number
shapeFlag: number
key: any
el: HostElement | null // 真实 DOM 引用
}
3.2 patch 过程
function patch(n1, n2, container) {
if (n1 === n2) return
if (n1 && !isSameVNode(n1, n2)) {
unmount(n1)
n1 = null
}
switch (n2.type) {
case Text: processText(n1, n2, container); break
case Fragment: processFragment(n1, n2, container); break
case Static: processStatic(n1, n2, container); break
default:
if (shapeFlag & ShapeFlags.COMPONENT) processComponent(n1, n2, container)
else processElement(n1, n2, container)
}
}
3.3 双端 Diff 算法(Vue 2)
Vue 2 使用双端比较策略,四个指针(oldStart、oldEnd、newStart、newEnd)同时向中间靠拢:
- 新旧头头比较
- 新旧尾尾比较
- 旧头 vs 新尾
- 旧尾 vs 新头
- 以上都不匹配时,用 key 建立映射表
优势:比普通 diff 更优地处理了 DOM 移动场景。 局限:对静态节点仍需遍历比较。
3.4 快速 Diff(Vue 3)
Vue 3 基于预处理 + 最长递增子序列(LIS)的优化策略:
1. 从头开始同步(sync from start)— 相同 key 的节点逐个比较直至不同
2. 从尾开始同步(sync from end)— 尾部相同 key 节点逐个比较直至不同
3. 剩余节点处理:
a. 新节点多 → 挂载新节点
b. 旧节点多 → 卸载旧节点
c. 位置不确定 → 构建新下标映射表 + 最长递增子序列(最小化移动)
最长递增子序列的应用:找到不需要移动的节点,只移动其他节点。例如:
旧序列: [A, B, C, D, E]
新序列: [A, C, B, E, D]
新索引数组: [0, 2, 1, 4, 3] // 元素在新列表中的位置
最长递增子序列: [0, 2, 4] // A、C、E 不需要移动
4. 编译优化
4.1 静态提升(Static Hoisting)
将静态节点提升到 render 函数之外,避免每次渲染都重新创建。
<!-- 模板 -->
<div><span>静态文本</span><span>{{ dynamic }}</span></div>
<!-- 编译后 -->
const _hoisted_1 = _createElementVNode("span", null, "静态文本", -1 /* HOISTED */)
function render() {
return _openBlock(), _createBlock("div", null, [
_hoisted_1,
_createElementVNode("span", null, _toDisplayString(dynamic), 1 /* TEXT */)
])
}
4.2 预字符串化(Static Stringify)
连续多个静态节点(阈值默认 20 个)合并为字符串,减少 VNode 创建。
<div>
<span>1</span><span>2</span><span>3</span>... <!-- 20+ 个静态 span -->
</div>
<!-- 编译为 -->
_createStaticVNode("<span>1</span><span>2</span><span>3</span>...")
比 createTextVNode 粒度更大、性能更好。
4.3 PatchFlags
编译时标记动态内容的类型,patch 阶段跳过静态部分。
enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 2, // 动态 class
STYLE = 4, // 动态 style
PROPS = 8, // 动态属性(非 class/style)
FULL_PROPS = 16, // 动态 key 的属性
HYDRATE_EVENTS = 32,// 需要水合事件
STABLE_FRAGMENT = 64,
KEYED_FRAGMENT = 128,
UNKEYED_FRAGMENT = 256,
NEED_PATCH = 512,
DYNAMIC_SLOTS = 1024,
// ...
HOISTED = -1, // 静态提升节点
BAIL = -2 // 退出优化,回退完整 diff
}
4.4 Block Tree
Vue 3 将模板结构化分为 Block(带动态子节点的节点),形成树状结构:
Block 树结构:
div (block) [dynamicChildren = [span, p]]
├── span 动态文本 ← 加入 dynamicChildren
├── p :title 动态属性 ← 加入 dynamicChildren
└── div (block)
└── input v-model ← 加入子 block 的 dynamicChildren
- 只有
dynamicChildren变化时才需要 diff - 静态结构变化(v-if/v-for)会创建新的 Block
- patch 时只遍历
dynamicChildren,跳过静态部分
4.5 靶向更新(Targeted Update)
结合 PatchFlags 和 Block Tree,Vue 3 在 patch 时只更新标记的动态内容,不再逐层遍历整个 VNode 树,实现 O(动态节点数量) 的更新复杂度。
5. 组件
5.1 Props / Emits / Expose
defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete'): void
}>()
// expose 暴露给父组件通过 ref 访问
defineExpose({
reset() { count.value = 0 }
})
5.2 Slots
<!-- 默认插槽 -->
<slot />
<!-- 具名插槽 -->
<slot name="header" />
<!-- 作用域插槽 -->
<slot name="item" :data="item" :index="i" />
<!-- 使用 -->
<template #item="{ data, index }">
<span>{{ index }}: {{ data.name }}</span>
</template>
5.3 动态组件 & 异步组件 & Suspense
<!-- 动态组件 -->
<component :is="currentComponent" />
<!-- 异步组件 -->
<script setup>
const Heavy = defineAsyncComponent(() => import('./HeavyComp.vue'), {
loadingComponent: Loading,
delay: 200, // 延迟显示 loading
timeout: 5000, // 超时错误
errorComponent: ErrorComp
})
</script>
<!-- Suspense -->
<Suspense>
<template #default>
<AsyncComponent /> <!-- 异步依赖 resolved 前显示 fallback -->
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
defineAsyncComponent 返回一个高阶组件,内部处理加载状态、超时、错误等。
5.4 Teleport
将子节点渲染到 DOM 的指定位置(如 body、modal 根节点),避免 CSS 层级问题。
<Teleport to="body">
<Modal :show="showModal" />
</Teleport>
6. Vue Router
6.1 Hash vs History 模式
| 模式 | URL | 原理 | 后端配置 |
|---|---|---|---|
| hash | /#/path |
hashchange 事件,不会发送到服务端 |
不需要 |
| history | /path |
pushState + popstate,需服务端 fallback |
需要(Nginx try_files /index.html) |
6.2 动态路由
const routes = [
{ path: '/users/:id', component: UserDetail },
// 正则匹配
{ path: '/articles/:slug(\\d+)', component: ArticleDetail },
]
// 通过 useRoute().params.id 获取
6.3 导航守卫
// 全局前置守卫
router.beforeEach((to, from) => {
if (!isAuthenticated && to.name !== 'login') return '/login'
})
// 全局解析守卫
router.beforeResolve((to) => {})
// 全局后置钩子
router.afterEach((to, from) => { sendAnalytics() })
// 路由级守卫
{
path: '/admin',
component: Admin,
beforeEnter: (to) => { /* ... */ }
}
// 组件内守卫
onBeforeRouteLeave((to, from) => {
if (hasUnsaved) return '确认离开?'
})
onBeforeRouteUpdate(async (to) => {
await loadData(to.params.id)
})
6.4 路由懒加载
const UserDetail = () => import('@/views/UserDetail.vue')
// 配合 webpack 注释或 Vite 的 rollupOptions 做代码分割
const UserDetail = () => import(/* webpackChunkName: "user" */ '@/views/UserDetail.vue')
7. Pinia(状态管理)
7.1 Option Store vs Setup Store
// Option Store(类似 Vuex)
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() { this.count++ }
}
})
// Setup Store(组合式风格)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, doubled, increment }
})
- Setup Store 更灵活,可混合 composables
- Option Store 更清晰,DevTools 支持更好
- 推荐团队统一风格,优先 Setup Store
7.2 state / getters / actions
// 直接修改
store.count++
store.$patch({ count: store.count + 1 })
store.$patch((state) => state.count++)
// 替换整个 state
store.$state = { count: 10 }
// 重置
store.$reset()
// 订阅
store.$subscribe((mutation, state) => {})
7.3 $patch
$patch 将多次修改合并为一次通知,内部遍历 key 或执行回调,之后触发一次订阅。
7.4 持久化插件
// pinia-plugin-persistedstate
export const useStore = defineStore('main', {
state: () => ({ count: 0 }),
persist: {
key: 'my-store',
storage: sessionStorage,
paths: ['count'], // 只持久化特定字段
}
})
7.5 与 Vuex 的区别
| 特性 | Pinia | Vuex |
|---|---|---|
| 架构 | 去中心化(多个独立 store) | 单一全局 Store |
| TypeScript | 原生完美支持 | 需要大量类型声明 |
| Mutations | 无(合并到 actions) | 有(mutation + action 分离) |
| DevTools | 支持 | 支持 |
| 体积 | ~1KB | 更重 |
| 模块/命名空间 | 自动隔离 | 需 namespaced: true |
8. 原理进阶
8.1 nextTick 原理
function nextTick(fn?) {
return fn ? Promise.resolve().then(fn) : Promise.resolve()
}
内部用 Promise.then(fallback: MutationObserver → setImmediate → setTimeout)实现微任务调度。
Vue 响应式更新是同步修改、异步批处理的:trigger 时将一个 flush job 加入微任务队列,所有同步代码执行完后统一执行 DOM 更新。
8.2 keep-alive(LRU 缓存)
<KeepAlive> 内部实现 LRU(Least Recently Used) 缓存策略:
- 缓存组件实例到
cache对象(Map) - 用
keys数组维护访问顺序 - 超出
max时淘汰最久未使用的组件实例 - 被缓存的组件会触发
activated/deactivated生命周期
// 伪代码
if (cache.size >= max) {
const oldestKey = keys[0]
pruneCacheEntry(oldestKey) // 移除最久未使用的
}
keys.push(key) // 最新使用
8.3 v-model 原理
Vue 3 中 v-model 是 :modelValue + @update:modelValue 的语法糖。
<!-- 等价 -->
<input v-model="text" />
<input :value="text" @input="text = $event.target.value" />
<!-- 自定义组件 -->
<Child v-model="val" />
<!-- 等价于 -->
<Child :modelValue="val" @update:modelValue="val = $event" />
<!-- 多个 v-model -->
<Child v-model:title="title" v-model:content="content" />
8.4 v-if vs v-show
| 特性 | v-if | v-show |
|---|---|---|
| 渲染 | 条件性创建/销毁 DOM | 始终渲染,切换 display |
| 初始化开销 | 条件假时不渲染 | 始终渲染 |
| 切换开销 | 销毁/重建组件 + DOM | 仅修改 CSS |
| 适用场景 | 运行时切换少的场景 | 频繁切换的场景 |
8.5 v-for 中 key 的作用
Vue 的 Diff 算法以 key 作为节点身份标识:
- 有 key → 尽量复用/移动节点,减少 DOM 操作
- 同 key 节点比较后 patch,不同 key 视为不同节点
- 无 key → 默认 inline 比较(索引对比),可能导致不必要的 DOM 复用和状态错乱
最佳实践:用稳定且唯一的 key(如数据 id),避免用 index 或随机值。
8.6 computed 的惰性求值
详见 2.3 节。核心:ComputedRefImpl 通过 _dirty 标志实现懒缓存,依赖变化仅标记 dirty,不做实际计算,直到被读取。
9. 性能优化
9.1 组件懒加载
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
仅在需要时加载组件代码。结合 Vue Router 的路由懒加载效果更佳。
9.2 合理使用 shallowRef
处理大型数据(图表数据、长列表、不可变数据)时使用 shallowRef 避免深度响应式的开销:
const largeDataset = shallowRef([])
// 修改时替换整个引用
largeDataset.value = newData
9.3 大列表虚拟滚动
使用虚拟滚动库(如 vue-virtual-scroller、vueuc 的 LazyVirtualList)只渲染可视区域内的节点:
- 固定容器高度
- 计算总滚动高度(撑开滚动条)
- 根据 scrollTop 计算可视范围,只渲染可见项
9.4 v-memo
Vue 3.2+,跳过指定依赖未变化时的子节点渲染:
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.updated]">
<!-- 仅当 item.id 或 item.updated 变化时重新渲染 -->
<ExpensiveComponent :data="item" />
</div>
注意:v-memo 与 v-for 共用时需配合有 key 的 v-for。
9.5 KeepAlive
缓存频繁切换的动态组件:
<KeepAlive :max="10">
<component :is="tabComponent" />
</KeepAlive>
避免组件重复销毁/重建,配合 max 限制缓存数量防止内存泄漏。