banner
Hi my new friend!

Vue3 响应式原理深度解析

Scroll down

前言

Vue3 的响应式系统是整个框架的基石。与 Vue2 基于 Object.defineProperty 的实现不同,Vue3 全面采用了 ES6 的 Proxy 对象来构建响应式系统。这一转变不仅解决了 Vue2 中无法检测属性添加/删除、数组索引修改等"历史遗留问题",还带来了更好的性能表现。

本文将从源码层面,深入剖析 Vue3 响应式系统的核心实现。

为什么选择 Proxy

Object.defineProperty 存在几个无法回避的局限:

  1. 无法检测对象属性的新增和删除:Vue2 需要使用 Vue.set / Vue.delete 来弥补
  2. 数组变更检测不完整:无法检测通过索引直接设置数组元素,以及修改 length 属性
  3. 需要递归遍历对象:初始化时必须深度遍历所有属性,对深层嵌套的大对象性能开销大
  4. 无法拦截 Map、Set、WeakMap、WeakSet:这些集合类型的操作完全无法被追踪

Proxy 可以拦截 13 种操作,包括 getsetdeletePropertyhasownKeys 等,天然支持上述所有场景。

核心模块分析

Vue3 响应式系统的核心代码位于 packages/reactivity 目录下,主要包含以下模块:

1. reactive — 对象响应式的入口

reactive 函数通过 createReactiveObject 创建响应式对象:

ts
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 非对象类型直接返回
  if (!isObject(target)) {
    return target
  }
  // 已经代理过的对象直接返回缓存
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 创建 Proxy
  const proxy = new Proxy(target, baseHandlers)
  proxyMap.set(target, proxy)
  return proxy
}

关键设计点:

  • 使用 WeakMap 缓存已代理对象,确保同一个原始对象始终返回同一个 Proxy 实例
  • 对数组、普通对象和 Map/Set 集合类型使用不同的 Handler 策略

2. effect — 副作用追踪系统

effect 是响应式系统的核心调度器。它的工作流程可以概括为三步:

第一步:依赖收集(Track)

effect 内部的函数首次执行时,访问到的每一个响应式属性都会被"追踪"。具体实现通过一个全局的 activeEffect 变量,将当前正在执行的 effect 函数与所访问的属性建立映射关系。

ts
// 简化的 track 逻辑
function track(target, type, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

这里使用了 targetMapWeakMap<object, Map<key, Set<effect>>>)这样的三层数据结构来维护依赖关系。

第二步:派发更新(Trigger)

当响应式数据发生变化时,Vue3 会查找所有依赖该属性的 effect 函数并重新执行。这里的调度策略非常关键——Vue3 使用 scheduler 队列来批量处理更新,避免在同一轮事件循环中重复渲染。

第三步:清理依赖

每次 effect 重新执行前,都会先清理上一次的依赖关系,确保不会出现"隐式依赖"导致的意外更新。这是通过 cleanupEffect 函数实现的。

3. ref — 原始值的响应式包装

ref 本质上是对 reactive 的补充。由于 Proxy 只能代理对象,对于原始类型(number、string、boolean 等),Vue3 通过一个带有 .value 访问器的对象来实现响应式:

ts
class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true

  constructor(value: T) {
    this._value = toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._value = toReactive(newVal)
      triggerRefValue(this)
    }
  }
}

computed 的实现

computed 是 effect 的一个特殊变体。它具有两个关键特性:

  1. 惰性求值(Lazy Evaluation):只有访问 .value 时才执行计算函数
  2. 缓存机制:依赖不变时直接返回缓存值,不重复执行

实现原理是通过一个 dirty 标志位来控制——当依赖变化时标记为 dirty,下次访问时重新计算并重置标志位。

总结

Vue3 的响应式系统是一个精心设计的"发布-订阅"模型:

  • reactive / ref 负责将数据转换为"可观察"对象
  • effect 负责注册"观察者"
  • track / trigger 负责建立和维护依赖关系
  • scheduler 负责调度更新,保证批量异步执行

这种设计在保持简洁 API 的同时,实现了细粒度的响应式追踪。理解其内部机制,有助于在实际项目中更好地利用 Vue3 的响应式能力,也能在遇到性能问题时做出更精准的诊断。

  • 本文作者:async
  • 本文链接:
  • 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
其他文章