HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • Vue & CSS 进阶

    • 第0章:工具链与开发体验
    • 第1章:TypeScript 核心
    • 第2章:CSS 现代能力
    • 第3章:Vue3 核心与原理
    • 第4章:路由与状态管理
    • 第5章:工程化与性能
    • 第6章:测试与质量
    • 第7章:CSS 进阶专题
    • 第8章:项目实战A - SaaS仪表盘
    • 第9章:项目实战B - 内容社区
    • 第10章:Vue 内核深入
    • 第11章:微前端与部署
    • CSS 变量体系与主题系统深度指南
    • 前端安全与防护专题
    • CSS 专题:动效优化与微交互
    • CSS 专题:图片与图形处理
    • 实时通信与WebSocket专题
    • 开发工具与调试技巧专题
    • 新兴技术与未来趋势专题
    • 移动端开发与PWA专题
    • 附录A:代码片段库
    • 附录B:练习题集

第3章:Vue3 核心与原理

深入 Vue3 的内部机制,理解响应式系统、模板编译、渲染流程等核心原理。掌握这些原理不仅能提升开发效率,更能为面试和架构设计打下坚实基础。

📋 本章内容

  • 响应式系统原理
  • 模板编译机制
  • 渲染器与虚拟 DOM
  • 组合式 API 最佳实践
  • 指令与插件系统
  • 实战练习

🎯 学习目标

  • 理解 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 有以下优势:

  1. 完整的对象监听:可以监听数组索引、对象属性的添加/删除
  2. 更好的性能:Proxy 是原生支持,性能更好
  3. 更简洁的实现:不需要递归遍历对象属性
// 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 3 响应式系统源码
  • Vue 3 编译系统源码
  • Vue 3 渲染器源码
  • Vue 3 组合式 API 指南

下一章预告:我们将学习 Vue Router 和 Pinia 状态管理,掌握路由配置、导航守卫、状态管理的最佳实践,为构建复杂的单页应用打下基础。

Prev
第2章:CSS 现代能力
Next
第4章:路由与状态管理