Vue.js 作为当前最受欢迎的前端框架之一,其响应式系统是其核心特性。从 Vue2 到 Vue3,响应式系统经历了重大的架构升级和性能优化。本文将深入探讨这一演进过程,帮助开发者理解两个版本响应式系统的设计思想、实现原理以及它们之间的差异。

Vue2 响应式系统回顾

观察者模式的应用

在深入了解 Vue2 响应式系统之前,我们先来理解其背后的设计模式——观察者模式。

想象这样一个场景:你想要买一个三折叠,但是目前店里还没货。那你如何才能及时买到呢?

有两种不太理想的方案:

  1. 你每天去一趟商场检查是否到货
  2. 店里商品到货后向所有顾客发出通知

这两种方案都存在问题:要么让你浪费时间,要么让商店浪费资源。

解决方案:让有需求的顾客(观察者/watcher)主动订阅,之后商店(发布者/dep)只需要给订阅了的用户发送通知。

Vue2 响应式工作机制

Vue2 的响应式系统正是基于观察者模式实现的:

  1. 数据劫持:data 中的数据会被 Vue 遍历,通过 Object.defineProperty 生成 getter 和 setter,这样当访问或设置属性时,Vue 就有机会做一些额外的事情。

  2. 依赖收集:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据属性记录为依赖。

  3. 派发更新:当依赖项的 setter 触发时,会通知 watcher,从而使关联的组件重新渲染。

    请添加图片描述

核心组件详解

1. Observer(数据劫持)

Observer 的工作是把一个普通的对象转换为响应式的对象。它通过 Object.defineProperty 把对象的每个属性转换为带有 getter 和 setter 的属性。

请添加图片描述

// 核心实现逻辑
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象,确保深层属性也具有响应性
  observe(val)
  
  // 每个属性对应一个依赖管理器
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集:将当前活跃的 Watcher 添加到依赖列表
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 新值如果是对象,需要递归处理为响应式
      observe(newVal)
      // 通知所有依赖更新
      dep.notify()
    }
  })
}

// 递归处理对象的所有属性
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  new Observer(obj)
}

class Observer {
  constructor(value) {
    this.value = value
    if (Array.isArray(value)) {
      // 处理数组:改写数组原型方法
      this.observeArray(value)
      value.__proto__ = arrayMethods
    } else {
      // 处理对象:遍历所有属性并定义响应式
      this.walk(value)
    }
  }
  
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
  
  observeArray(arr) {
    arr.forEach(item => observe(item))
  }
}

Observer 是 Vue 内部的构造器,在组件生命周期中,这件事发生在 beforeCreate 之后,created 之前。实现上:

  • 它会递归遍历对象的所有属性,完成深度的属性转换。
  • 由于遍历时只能遍历到对象的当前属性,因此无法监测动态增加或删除的属性,需要使用 $set$delete 方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。
  • 对于数组,需要重写原型方法来监听变化。

总的来说,Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 Vue 感知到。

2. Dep(发布者/依赖管理器)

Dep 是依赖管理器,核心功能如下:

  • 添加观察者:当读取响应式对象的某个属性时,进行依赖收集
  • 通知观察者:当改变某个属性时,派发更新

请添加图片描述

class Dep {
  static target = null // 当前活跃的 Watcher
  constructor() {
    this.subs = [] // 存储依赖的 Watcher
  }
  
  // 添加依赖
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  // 通知所有依赖更新
  notify() {
    const subs = this.subs.slice()
    subs.forEach(sub => sub.update())
  }
  
  // 添加 Watcher 到依赖列表
  addSub(sub) {
    this.subs.push(sub)
  }
  
  // 移除 Watcher
  removeSub(sub) {
    remove(this.subs, sub)
  }
}
3. Watcher(观察者)

Watcher 是观察者,当依赖的数据发生变化时,Dep 会通知 Watcher 调用 update 方法来更新数据。

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this.cb = cb
    this.deps = [] // 存储该 Watcher 依赖的 Dep
    this.depIds = new Set() // 用于去重
    
    // 解析表达式,生成更新函数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    
    // 初始化时触发依赖收集
    this.value = this.get()
  }
  
  // 触发 getter 并收集依赖
  get() {
    Dep.target = this // 设置当前活跃的 Watcher
    const value = this.getter.call(this.vm, this.vm) // 执行 getter,触发数据的 getter
    Dep.target = null // 重置
    return value
  }
  
  // 接收更新通知
  update() {
    // 放入调度器队列
    queueWatcher(this)
  }
  
  // 实际执行更新
  run() {
    const newValue = this.get()
    const oldValue = this.value
    if (newValue !== oldValue) {
      this.value = newValue
      this.cb.call(this.vm, newValue, oldValue)
    }
  }
  
  // 添加依赖
  addDep(dep) {
    const id = dep.id
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}
4. Scheduler(调度器)

Dep 通知 watcher 之后,如果 watcher 执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下。为了避免这个问题,Vue2 引入了调度器。

调度器维护维护一个执行队列,同一个 watcher 仅存在一次,队列中的 watcher 不是立即执行,而是通过 nextTick 把更新操作放入微队列中异步执行,也就是说,当响应式数据变化时,render 函数的执行是异步的,并且在微队列中。

const queue = []
let has = {} // 用于去重
let waiting = false

function queueWatcher(watcher) {
  const id = watcher.id
  if (!has[id]) {
    has[id] = true
    queue.push(watcher)
    
    // 延迟执行 flushSchedulerQueue
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue() {
  queue.sort((a, b) => a.id - b.id) // 按 id 排序,确保父组件先于子组件更新
  
  // 执行所有 Watcher 的 run 方法
  for (let i = 0; i < queue.length; i++) {
    const watcher = queue[i]
    watcher.run()
  }
  
  // 重置队列
  resetSchedulerState()
}

// 利用 Promise/MutationObserver/setImmediate/setTimeout 实现异步更新
function nextTick(cb) {
  callbacks.push(cb)
  if (!pending) {
    pending = true
    if (typeof Promise !== 'undefined') {
      Promise.resolve().then(flushCallbacks)
    } else if (typeof MutationObserver !== 'undefined') {
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(0))
      observer.observe(textNode, { characterData: true })
      textNode.data = 1
    } else if (typeof setImmediate !== 'undefined') {
      setImmediate(flushCallbacks)
    } else {
      setTimeout(flushCallbacks, 0)
    }
  }
}

Vue2 响应式整体流程如下:

请添加图片描述

尽管 Vue2 的响应式系统设计精巧,但仍存在一些局限性:

  1. 拦截范围有限Object.defineProperty 只能拦截对象特定属性的读写操作
  2. 无法检测新增/删除属性:需要使用 $set$delete
  3. 数组监听复杂:需要重写数组原型方法
  4. 性能开销:需要递归遍历所有属性

Vue3 响应式系统的革新

Vue3 对响应式系统进行了彻底重构,基于 Proxy 实现了更强大、更灵活的响应式能力,同时保持了更好的性能和开发体验,主要体现在三个方面:

1. 数据拦截方式的变化

Vue2:使用 Object.defineProperty 进行拦截
Vue3:使用 Proxy + Object.defineProperty 进行拦截

  • ref:使用 Object.defineProperty + Proxy 方式
  • reactive:使用 Proxy 方式

两者的共同点

  • 都可以针对对象成员拦截
  • 都可以实现深度拦截

两者的差异点

  • 拦截广度
    • Object.defineProperty 针对对象特定属性读写操作进行拦截,新增/删除的属性无法侦测
    • Proxy 针对整个对象的多种操作进行拦截,包括属性读取、赋值、删除、描述符获取等
  • 性能:在大多数场景下,Proxy 比 Object.defineProperty 效率更高,拦截方式更加灵活
2. 创建响应式数据的变化

Vue2:通过 data 选项创建响应式数据
Vue3:通过 refreactive 等方法创建响应式数据

ref 的实现原理
class RefImpl<T> {
  private _value: T
  private _rawValue: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 有可能是原始值,有可能是 reactive 返回的 proxy
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 收集依赖
    return this._value
  }

  set value(newVal) {
    // 派发更新
  }
}

// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
reactive 的实现原理
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // 创建 Proxy 代理对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

export function reactive(target: object) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}
3. 依赖收集机制的变化

Vue2:Watcher + Dep

  • 每个响应式属性都有一个 Dep 实例,用于依赖收集
  • Dep 内部包含一个数组,存储依赖这个属性的所有 watcher
  • 当属性值发生变化,dep 通知所有 watcher 进行更新

Vue3:WeakMap + Map + Set

  • Vue3 的依赖收集粒度更细
  • WeakMap 的键对应响应式对象,值是一个 Map
  • Map 的键是该对象的属性,值是一个 Set
  • Set 里面存储了所有依赖于这个属性的 effect 函数
// Vue3 依赖收集结构
const targetMap = new WeakMap()
// targetMap: WeakMap {
//   target1: Map {
//     'property1': Set { effect1, effect2 },
//     'property2': Set { effect3 }
//   }
// }

Vue3 响应式系统的优势

  1. 更强的拦截能力:Proxy 可以拦截更多操作类型
  2. 更好的性能:按需代理,避免了递归遍历所有属性
  3. 更细粒度的依赖追踪:从组件级别细化到 effect 函数级别
  4. 更好的 TypeScript 支持:类型推导更加准确
  5. 更灵活的 API:ref 和 reactive 提供了更多选择

总结

Vue3 的响应式系统相比 Vue2 有了质的飞跃:

  1. 数据拦截方面:Vue2 受限于 Object.defineProperty 的能力,不得不通过各种补丁(如 $set、数组方法重写)适配开发需求;Vue3 则借助 Proxy 实现了更自然的响应式体验。
  2. 语法层面":Vue2 的响应式系统隐藏在 data 选项背后,开发者难以直接操控;Vue3 提供的 reactiveref 等 API 让响应式机制更透明,也更易于扩展。
  3. 依赖收集方面:Vue2 的 Watcher 与组件强绑定;Vue3 的副作用函数模型使响应式数据可以脱离组件单独使用,更适合跨组件状态管理。

更多推荐