Vue 2 响应式系统的核心实现

179 阅读3分钟

Vue 2 的响应式系统是通过 Object.defineProperty 实现的。Vue 2 的核心思想是通过递归遍历数据对象,对每个属性进行劫持(即使用 Object.defineProperty 定义 getter 和 setter),从而在数据变化时触发依赖更新。

1. 核心概念

  • Observer:递归遍历对象的所有属性,将其转换为响应式数据。
  • Dep(Dependency) :每个属性都有一个对应的 Dep 实例,用于管理依赖(Watcher)。
  • Watcher:观察者,负责监听数据变化并执行回调(如更新视图)。

2. 实现步骤

(1)将数据转换为响应式

Vue 2 使用 Object.defineProperty 对对象的每个属性进行劫持。

function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性都有一个 Dep 实例

  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) { // 如果当前有 Watcher 在收集依赖
        dep.depend(); // 将 Watcher 添加到 Dep 中
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify(); // 通知所有 Watcher 更新
    }
  });
}

(2)递归遍历对象

Vue 2 会递归遍历对象的所有属性,将其转换为响应式。

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }

  // 遍历对象的所有属性
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

(3)依赖收集(Dep)

Dep 是一个依赖管理器,用于存储所有依赖(Watcher)。

class Dep {
  constructor() {
    this.subs = []; // 存储 Watcher
  }

  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target); // 将当前 Watcher 添加到依赖中
    Dep.target.addDep(this); // Watcher 也记录 Dep
    }
  }

  notify() {
    this.subs.forEach(watcher => watcher.update()); // 通知所有 Watcher 更新
  }
}

Dep.target = null; // 全局变量,指向当前正在收集依赖的 Watcher

(4)观察者(Watcher)

Watcher 是观察者,负责监听数据变化并执行回调。

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    Dep.target = this; // 设置当前 Watcher
    this.value = this.vm[this.key]; // 触发 getter,收集依赖
    Dep.target = null; // 重置
  }

  update() {
    const newValue = this.vm[this.key];
    if (newValue !== this.value) {
      this.value = newValue;
      this.cb(newValue); // 执行回调
    }
  }
}

(5)初始化响应式数据

在 Vue 实例化时,会调用 observe 将数据转换为响应式。

class Vue {
  constructor(options) {
    this._data = options.data;
    observe(this._data); // 将数据转换为响应式
  }
}

3. 数组的响应式处理

Vue 2 对数组的处理与对象不同,因为 Object.defineProperty 无法直接监听数组的变化(如 pushpop 等操作)。Vue 2 通过重写数组的原型方法来实现数组的响应式。

(1)重写数组方法

Vue 2 创建了一个新的数组原型,并重写了会改变数组的方法(如 pushpopsplice 等)。

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    value: function(...args) {
      const result = original.apply(this, args);
      const ob = this.__ob__; // 获取 Observer 实例
      ob.dep.notify(); // 通知依赖更新
      return result;
    },
    enumerable: false,
    writable: true,
    configurable: true
  });
});

(2)将数组转换为响应式

在 observe 函数中,如果检测到是数组,则将其原型指向重写后的 arrayMethods

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }

  if (Array.isArray(obj)) {
    obj.__proto__ = arrayMethods; // 重写数组原型
    obj.forEach(item => observe(item)); // 递归处理数组元素
  } else {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

4. 总结

Vue 2 的响应式系统核心是通过 Object.defineProperty 对对象的属性进行劫持,并通过 Dep 和 Watcher 实现依赖收集和更新。对于数组,Vue 2 通过重写数组的原型方法来实现响应式。

优点

  • 实现简单,兼容性好(支持 IE9+)。
  • 对对象的属性劫持非常精细。

缺点

  • 无法监听新增或删除的属性(需要使用 Vue.set 或 Vue.delete)。
  • 对数组的处理需要额外逻辑。

在 Vue 3 中,响应式系统改用了 Proxy,解决了 Vue 2 中的一些局限性。