第3章:Vue3 核心与原理
深入 Vue3 的内部机制,理解响应式系统、模板编译、渲染流程等核心原理。掌握这些原理不仅能提升开发效率,更能为面试和架构设计打下坚实基础。
📋 本章内容
🎯 学习目标
- 理解 Vue3 响应式系统的核心机制
- 掌握模板编译的完整流程
- 了解虚拟 DOM 的 diff 算法
- 学会设计可复用的组合式函数
- 掌握自定义指令和插件的开发
⚡ 响应式系统原理
核心数据结构
Vue3 的响应式系统基于 Proxy 实现,核心数据结构如下:
// 核心数据结构
const targetMap = new WeakMap<any, Map<any, Set<Function>>>()
// 当前活跃的副作用函数
let activeEffect: Function | null = null
// 副作用函数栈(处理嵌套 effect)
const effectStack: Function[] = []
极简响应式实现
让我们从零开始实现一个简化版的响应式系统:
// 1. 依赖收集和触发机制
function effect(fn: Function) {
const _effect = () => {
try {
effectStack.push(_effect)
activeEffect = _effect
fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1] || null
}
}
_effect()
return _effect
}
function track(target: any, key: any) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target: any, key: any) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) {
deps.forEach((effect: Function) => effect())
}
}
// 2. 响应式对象实现
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key)
return result
},
set(target, key, value, receiver) {
const oldValue = target[key as keyof T]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
// 3. ref 实现
function ref<T>(value: T) {
return reactive({
value
})
}
// 4. computed 实现
function computed<T>(getter: () => T) {
let value: T
let dirty = true
const effectFn = effect(() => {
if (dirty) {
value = getter()
dirty = false
}
})
return {
get value() {
if (dirty) {
value = getter()
dirty = false
}
return value
}
}
}
Vue3 响应式系统源码解析
让我们深入 Vue3 的实际源码:
// packages/reactivity/src/reactive.ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> {
// 如果已经是响应式对象,直接返回
if (target && (target as Target)[ReactiveFlags.IS_REACTIVE]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 检查是否已经是代理对象
if (target[ReactiveFlags.RAW]) {
return target
}
// 创建代理对象
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
deleteProperty,
has,
ownKeys
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 特殊键的处理
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
// 依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
const res = Reflect.get(target, key, receiver)
// 深度响应式处理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
const result = Reflect.set(target, key, value, receiver)
// 触发更新
if (target === toRaw(receiver)) {
if (!shallow) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
响应式系统的优势
相比 Vue2 的 Object.defineProperty,Vue3 的 Proxy 有以下优势:
- 完整的对象监听:可以监听数组索引、对象属性的添加/删除
- 更好的性能:Proxy 是原生支持,性能更好
- 更简洁的实现:不需要递归遍历对象属性
// Vue2 的限制
const obj = { a: 1 }
Vue.set(obj, 'b', 2) // 需要特殊方法添加响应式属性
// Vue3 的优势
const obj = reactive({ a: 1 })
obj.b = 2 // 直接添加,自动响应式
🔧 模板编译机制
编译流程概览
Vue 的模板编译分为三个阶段:
模板字符串 → AST → 渲染函数 → 虚拟 DOM
1. 模板解析(Parse)
将模板字符串解析为抽象语法树(AST):
// 简化的解析器实现
interface ASTNode {
type: string
tag?: string
attrs?: Array<{ name: string; value: string }>
children?: ASTNode[]
text?: string
}
function parse(template: string): ASTNode {
const stack: ASTNode[] = []
let root: ASTNode | null = null
let currentParent: ASTNode | null = null
// 正则表达式匹配标签
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*)>/g
const textRegex = /([^<]+)/g
let match
let lastIndex = 0
while ((match = tagRegex.exec(template)) !== null) {
const [fullMatch, tagName, attrs] = match
const startIndex = match.index
// 处理文本节点
if (startIndex > lastIndex) {
const text = template.slice(lastIndex, startIndex).trim()
if (text) {
const textNode: ASTNode = {
type: 'text',
text
}
if (currentParent) {
currentParent.children = currentParent.children || []
currentParent.children.push(textNode)
}
}
}
// 处理开始标签
if (!fullMatch.startsWith('</')) {
const element: ASTNode = {
type: 'element',
tag: tagName,
attrs: parseAttrs(attrs),
children: []
}
if (!root) {
root = element
}
if (currentParent) {
currentParent.children!.push(element)
}
stack.push(element)
currentParent = element
} else {
// 处理结束标签
stack.pop()
currentParent = stack[stack.length - 1] || null
}
lastIndex = tagRegex.lastIndex
}
return root!
}
function parseAttrs(attrString: string) {
const attrs: Array<{ name: string; value: string }> = []
const attrRegex = /([a-zA-Z-]+)(?:="([^"]*)")?/g
let match
while ((match = attrRegex.exec(attrString)) !== null) {
attrs.push({
name: match[1],
value: match[2] || ''
})
}
return attrs
}
2. 代码生成(Codegen)
将 AST 转换为渲染函数:
function generate(ast: ASTNode): string {
const code = ast ? genElement(ast) : '_c("div")'
return `with(this){return ${code}}`
}
function genElement(node: ASTNode): string {
if (node.type === 'text') {
return `_v(${JSON.stringify(node.text)})`
}
if (node.type === 'element') {
const { tag, attrs, children } = node
// 生成属性
const props = attrs ? genProps(attrs) : 'null'
// 生成子节点
const childrenCode = children ? genChildren(children) : 'null'
return `_c("${tag}", ${props}, ${childrenCode})`
}
return ''
}
function genProps(attrs: Array<{ name: string; value: string }>): string {
const props: string[] = []
for (const attr of attrs) {
if (attr.name.startsWith('v-')) {
// 处理指令
const directive = attr.name.slice(2)
props.push(`"${directive}":${attr.value}`)
} else {
// 处理普通属性
props.push(`"${attr.name}":"${attr.value}"`)
}
}
return `{${props.join(',')}}`
}
function genChildren(children: ASTNode[]): string {
return `[${children.map(child => genElement(child)).join(',')}]`
}
3. 实际编译示例
<!-- 模板 -->
<template>
<div class="container">
<h1>{{ title }}</h1>
<button @click="increment">Count: {{ count }}</button>
</div>
</template>
编译后的渲染函数:
function render() {
with(this) {
return _c('div',
{ class: 'container' },
[
_c('h1', [_v(_s(title))]),
_c('button',
{ on: { click: increment } },
[_v('Count: ' + _s(count))]
)
]
)
}
}
🎨 渲染器与虚拟 DOM
虚拟 DOM 结构
interface VNode {
type: string | Component
props?: Record<string, any>
children?: VNode[] | string
el?: Element
key?: string | number
shapeFlag: number
}
// 创建虚拟 DOM 节点
function createVNode(
type: string | Component,
props?: Record<string, any>,
children?: VNode[] | string
): VNode {
const vnode: VNode = {
type,
props,
children,
shapeFlag: getShapeFlag(type)
}
if (children) {
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
return vnode
}
Diff 算法核心
Vue3 的 diff 算法采用同层比较策略:
function patch(n1: VNode | null, n2: VNode, container: Element) {
// 如果类型不同,直接替换
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container)
break
case Fragment:
processFragment(n1, n2, container)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container)
}
}
}
function processElement(n1: VNode | null, n2: VNode, container: Element) {
if (n1 == null) {
mountElement(n2, container)
} else {
patchElement(n1, n2)
}
}
function patchElement(n1: VNode, n2: VNode) {
const el = (n2.el = n1.el!)
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 更新属性
patchProps(el, oldProps, newProps)
// 更新子节点
patchChildren(n1, n2, el)
}
function patchChildren(n1: VNode, n2: VNode, container: Element) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1
const { shapeFlag: shapeFlag, children: c2 } = n2
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新子节点是文本
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[])
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
// 新子节点是数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 都是数组,需要 diff
patchKeyedChildren(c1 as VNode[], c2 as VNode[], container)
} else {
// 新子节点是空
unmountChildren(c1 as VNode[])
}
} else {
// 旧子节点是文本或空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2 as VNode[], container)
}
}
}
}
最长递增子序列算法
Vue3 使用最长递增子序列算法优化列表更新:
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
🎯 组合式 API 最佳实践
自定义组合式函数
// useCounter 组合式函数
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count: readonly(count),
increment,
decrement,
reset
}
}
// useLocalStorage 组合式函数
export function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = localStorage.getItem(key)
const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue
const value = ref<T>(initialValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// useAsyncData 组合式函数
export function useAsyncData<T>(
key: string,
handler: () => Promise<T>,
options: {
server?: boolean
default?: () => T
transform?: (data: T) => any
} = {}
) {
const data = ref<T | null>(options.default?.() || null)
const error = ref<Error | null>(null)
const pending = ref(false)
const execute = async () => {
pending.value = true
error.value = null
try {
const result = await handler()
data.value = options.transform ? options.transform(result) : result
} catch (err) {
error.value = err as Error
} finally {
pending.value = false
}
}
// 服务端渲染时立即执行
if (options.server !== false) {
execute()
}
return {
data: readonly(data),
error: readonly(error),
pending: readonly(pending),
execute
}
}
依赖注入模式
// 定义注入键
const ThemeSymbol = Symbol('theme')
const ApiSymbol = Symbol('api')
// 提供者
export function provideTheme(theme: Ref<string>) {
provide(ThemeSymbol, theme)
}
export function provideApi(api: any) {
provide(ApiSymbol, api)
}
// 消费者
export function useTheme() {
const theme = inject<Ref<string>>(ThemeSymbol)
if (!theme) {
throw new Error('useTheme must be used within a theme provider')
}
return theme
}
export function useApi() {
const api = inject(ApiSymbol)
if (!api) {
throw new Error('useApi must be used within an api provider')
}
return api
}
// 使用示例
// App.vue
export default {
setup() {
const theme = ref('light')
provideTheme(theme)
const api = createApi()
provideApi(api)
}
}
// Child.vue
export default {
setup() {
const theme = useTheme()
const api = useApi()
return { theme, api }
}
}
🔌 指令与插件系统
自定义指令
// 全局指令
app.directive('focus', {
mounted(el: HTMLElement) {
el.focus()
}
})
// 局部指令
export default {
directives: {
focus: {
mounted(el: HTMLElement) {
el.focus()
}
}
}
}
// 高级指令:v-click-outside
export const clickOutside = {
mounted(el: HTMLElement, binding: any) {
el._clickOutside = (event: Event) => {
if (!(el === event.target || el.contains(event.target as Node))) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el: HTMLElement) {
document.removeEventListener('click', el._clickOutside)
delete el._clickOutside
}
}
// 使用
app.directive('click-outside', clickOutside)
插件开发
// 插件接口
interface Plugin {
install(app: App, ...options: any[]): void
}
// 示例:HTTP 插件
export interface HttpPluginOptions {
baseURL: string
timeout: number
interceptors?: {
request?: (config: any) => any
response?: (response: any) => any
}
}
export const HttpPlugin: Plugin = {
install(app: App, options: HttpPluginOptions) {
const http = createHttpClient(options)
// 全局属性
app.config.globalProperties.$http = http
// 提供/注入
app.provide('http', http)
// 全局方法
app.config.globalProperties.$get = http.get
app.config.globalProperties.$post = http.post
}
}
// 使用插件
app.use(HttpPlugin, {
baseURL: 'https://api.example.com',
timeout: 5000,
interceptors: {
request: (config) => {
config.headers.Authorization = `Bearer ${getToken()}`
return config
},
response: (response) => {
if (response.status === 401) {
// 处理未授权
redirectToLogin()
}
return response
}
}
})
🧪 实战练习
练习1:实现简化版响应式系统
// 实现一个简化版的响应式系统
class SimpleReactive {
private targetMap = new WeakMap()
private activeEffect: Function | null = null
reactive<T extends object>(target: T): T {
return new Proxy(target, {
get: (target, key, receiver) => {
this.track(target, key)
return Reflect.get(target, key, receiver)
},
set: (target, key, value, receiver) => {
const oldValue = target[key as keyof T]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
this.trigger(target, key)
}
return result
}
})
}
effect(fn: Function) {
const effect = () => {
this.activeEffect = effect
fn()
this.activeEffect = null
}
effect()
return effect
}
private track(target: any, key: any) {
if (!this.activeEffect) return
let depsMap = this.targetMap.get(target)
if (!depsMap) {
this.targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(this.activeEffect)
}
private trigger(target: any, key: any) {
const depsMap = this.targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) {
deps.forEach((effect: Function) => effect())
}
}
}
// 使用示例
const reactive = new SimpleReactive()
const state = reactive.reactive({ count: 0 })
reactive.effect(() => {
console.log('Count:', state.count)
})
state.count++ // 输出: Count: 1
state.count++ // 输出: Count: 2
练习2:实现虚拟 DOM 渲染器
// 简化的虚拟 DOM 渲染器
interface VNode {
type: string
props?: Record<string, any>
children?: VNode[] | string
el?: Element
}
class SimpleRenderer {
createElement(type: string) {
return document.createElement(type)
}
createText(text: string) {
return document.createTextNode(text)
}
setElementText(el: Element, text: string) {
el.textContent = text
}
insert(el: Element, parent: Element, anchor?: Element) {
parent.insertBefore(el, anchor || null)
}
remove(el: Element) {
const parent = el.parentNode
if (parent) {
parent.removeChild(el)
}
}
patchProps(el: Element, oldProps: Record<string, any>, newProps: Record<string, any>) {
// 移除旧属性
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 设置新属性
for (const key in newProps) {
const value = newProps[key]
if (key.startsWith('on')) {
// 事件处理
const eventName = key.slice(2).toLowerCase()
el.addEventListener(eventName, value)
} else {
el.setAttribute(key, value)
}
}
}
patch(n1: VNode | null, n2: VNode, container: Element) {
if (n1 && n1.type !== n2.type) {
this.unmount(n1)
n1 = null
}
if (n1 == null) {
this.mountElement(n2, container)
} else {
this.patchElement(n1, n2)
}
}
mountElement(vnode: VNode, container: Element) {
const el = this.createElement(vnode.type)
vnode.el = el
if (vnode.props) {
this.patchProps(el, {}, vnode.props)
}
if (typeof vnode.children === 'string') {
this.setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
this.patch(null, child, el)
})
}
this.insert(el, container)
}
patchElement(n1: VNode, n2: VNode) {
const el = n2.el = n1.el!
// 更新属性
this.patchProps(el, n1.props || {}, n2.props || {})
// 更新子节点
this.patchChildren(n1, n2, el)
}
patchChildren(n1: VNode, n2: VNode, container: Element) {
const c1 = n1.children
const c2 = n2.children
if (typeof c2 === 'string') {
if (typeof c1 === 'string') {
if (c1 !== c2) {
this.setElementText(container, c2)
}
} else {
this.setElementText(container, c2)
}
} else if (Array.isArray(c2)) {
if (Array.isArray(c1)) {
// 简单的 diff 算法
const len = Math.min(c1.length, c2.length)
for (let i = 0; i < len; i++) {
this.patch(c1[i], c2[i], container)
}
if (c2.length > c1.length) {
// 新增节点
for (let i = len; i < c2.length; i++) {
this.patch(null, c2[i], container)
}
} else if (c1.length > c2.length) {
// 删除节点
for (let i = len; i < c1.length; i++) {
this.unmount(c1[i])
}
}
} else {
// 旧节点是文本,清空并挂载新节点
this.setElementText(container, '')
c2.forEach(child => {
this.patch(null, child, container)
})
}
}
}
unmount(vnode: VNode) {
if (vnode.el) {
this.remove(vnode.el)
}
}
}
// 使用示例
const renderer = new SimpleRenderer()
const vnode1: VNode = {
type: 'div',
props: { class: 'container' },
children: [
{ type: 'h1', children: 'Hello' },
{ type: 'p', children: 'World' }
]
}
const vnode2: VNode = {
type: 'div',
props: { class: 'container' },
children: [
{ type: 'h1', children: 'Hello' },
{ type: 'p', children: 'Vue' }
]
}
const container = document.getElementById('app')!
renderer.patch(null, vnode1, container)
// 更新
setTimeout(() => {
renderer.patch(vnode1, vnode2, container)
}, 1000)
练习3:实现组合式函数库
// useDebounce 防抖
export function useDebounce<T>(value: Ref<T>, delay: number) {
const debouncedValue = ref(value.value)
let timeoutId: number | null = null
watch(value, (newValue) => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
onUnmounted(() => {
if (timeoutId) {
clearTimeout(timeoutId)
}
})
return debouncedValue
}
// useThrottle 节流
export function useThrottle<T>(value: Ref<T>, delay: number) {
const throttledValue = ref(value.value)
let lastUpdate = 0
watch(value, (newValue) => {
const now = Date.now()
if (now - lastUpdate >= delay) {
throttledValue.value = newValue
lastUpdate = now
}
})
return throttledValue
}
// useIntersectionObserver 交叉观察器
export function useIntersectionObserver(
target: Ref<Element | null>,
callback: (entries: IntersectionObserverEntry[]) => void,
options: IntersectionObserverInit = {}
) {
const isIntersecting = ref(false)
let observer: IntersectionObserver | null = null
watch(target, (newTarget) => {
if (observer) {
observer.disconnect()
}
if (newTarget) {
observer = new IntersectionObserver((entries) => {
isIntersecting.value = entries[0].isIntersecting
callback(entries)
}, options)
observer.observe(newTarget)
}
}, { immediate: true })
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
return { isIntersecting }
}
// 使用示例
export default {
setup() {
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 300)
const target = ref<Element | null>(null)
const { isIntersecting } = useIntersectionObserver(target, (entries) => {
console.log('Element is visible:', entries[0].isIntersecting)
})
return {
searchQuery,
debouncedQuery,
target,
isIntersecting
}
}
}
📚 最佳实践
1. 响应式数据设计
// ✅ 好的做法:扁平化状态结构
const state = reactive({
user: {
id: 1,
name: 'John',
email: 'john@example.com'
},
posts: [],
loading: false
})
// ❌ 避免:深层嵌套
const state = reactive({
app: {
user: {
profile: {
personal: {
name: 'John'
}
}
}
}
})
2. 组合式函数设计
// ✅ 好的做法:单一职责
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return { count: readonly(count), increment, decrement, reset }
}
// ❌ 避免:功能过于复杂
export function useEverything() {
// 包含太多不相关的功能
}
3. 性能优化
// ✅ 使用 shallowRef 优化大对象
const largeData = shallowRef(bigObject)
// ✅ 使用 markRaw 标记非响应式数据
const chart = markRaw(new Chart(canvas, options))
// ✅ 合理使用 computed 缓存
const expensiveValue = computed(() => {
return heavyCalculation(props.data)
})
🚨 常见问题
1. 响应式丢失
问题:解构赋值导致响应式丢失
// ❌ 错误
const { count } = reactive({ count: 0 })
// ✅ 正确
const state = reactive({ count: 0 })
const { count } = toRefs(state)
2. 循环引用
问题:响应式对象循环引用
// ❌ 错误
const obj = reactive({})
obj.self = obj
// ✅ 正确
const obj = reactive({})
obj.self = markRaw(obj)
3. 内存泄漏
问题:事件监听器未清理
// ✅ 正确
export default {
setup() {
const handleResize = () => {}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
}
}
📖 延伸阅读
下一章预告:我们将学习 Vue Router 和 Pinia 状态管理,掌握路由配置、导航守卫、状态管理的最佳实践,为构建复杂的单页应用打下基础。