Vue 2 的 响应式原理——Object.defineProperty

公司旧项目在使用vue2开发,管用react-hooks的我研究了一下它的底层原理,让我们更详细地解释 Vue 2 的 响应式原理,特别是 Object.defineProperty 是如何实现数据与视图之间的自动关联的。Vue 的响应式系统是其核心特性之一,它使得当数据发生变化时,视图能够自动更新,而无需手动操作 DOM。


1. 什么是响应式?

响应式 指的是当数据发生变化时,系统能够自动感知并做出相应的反应。在 Vue 中,这意味着当你修改了 data 中的某个属性时,Vue 会自动更新所有依赖于这个属性的视图部分。


2. Vue 2 的响应式实现原理

Vue 2 的响应式系统主要依赖于 Object.defineProperty。它的核心思想是通过 数据劫持依赖收集 来实现数据与视图的自动绑定。

步骤详解

步骤 1:数据劫持(Data Observation)

Vue 在初始化时,会遍历 data 对象中的所有属性,并使用 Object.defineProperty 将这些属性转换为 gettersetter。这样,每当访问或修改这些属性时,Vue 都能捕获到这些操作。

function defineReactive(obj, key, val) {
  // 每个属性都有一个依赖收集器(Dep)
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    enumerable: true, // 属性可枚举
    configurable: true, // 属性可配置
    get: function reactiveGetter() {
      // 收集依赖:当前属性被访问时,记录下哪些 Watcher 需要监听它
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return; // 如果值没有变化,不做任何操作
      val = newVal;
      // 触发更新:通知所有依赖该属性的 Watcher 进行更新
      dep.notify();
    }
  });
}

// 示例
const data = { message: 'Hello Vue!' };
defineReactive(data, 'message', data.message);

data.message; // 访问属性,触发 getter,收集依赖
data.message = 'Hello World!'; // 修改属性,触发 setter,通知更新

步骤 2:依赖收集(Dependency Collection)

在 Vue 中,视图渲染的过程是通过 Watcher 来完成的。Watcher 是一个观察者,它负责监听数据的变化并更新视图。

当 Vue 渲染组件时,会访问 data 中的属性。在访问这些属性时,getter 被触发,Vue 会将当前的 Watcher 添加到该属性的依赖列表中(即 Dep)。这个过程称为 依赖收集

class Dep {
  constructor() {
    this.subscribers = new Set(); // 存储 Watcher
  }

  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }

  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

Dep.target = null; // 全局变量,指向当前的 Watcher

class Watcher {
  constructor(updateCallback) {
    Dep.target = this; // 设置当前 Watcher
    this.updateCallback = updateCallback;
    this.update(); // 触发更新,从而进行依赖收集
    Dep.target = null; // 重置
  }

  update() {
    this.updateCallback();
  }
}

// 示例
const data = {};
defineReactive(data, 'message', 'Hello Vue!');

new Watcher(() => {
  console.log(`视图更新:${data.message}`);
});

data.message = 'Hello World!'; // 修改数据,触发视图更新

解释:

  1. Watcher 在初始化时会执行 update 方法,这个方法会访问 data.message
  2. 访问 data.message 时,getter 被触发,当前的 Watcher 被添加到 Dep 的订阅列表中。
  3. data.message 被修改时,setter 被触发,Dep 通知所有订阅的 Watcher 进行更新。

步骤 3:派发更新(Dispatching Updates)

当数据发生变化时,setter 不仅更新值,还会通知所有依赖该属性的 Watcher。Watcher 收到通知后,会执行相应的回调函数,通常是重新渲染视图。

// 继续上面的示例
data.message = 'Hello Vue 3!'; // 修改数据
// 输出:视图更新:Hello Vue 3!

3. 响应式系统的完整流程

结合上述步骤,Vue 2 的响应式系统的工作流程如下:

  1. 初始化阶段
    • Vue 遍历 data 对象,使用 Object.defineProperty 将每个属性转换为 getter 和 setter。
    • 每个属性都有一个对应的 Dep 实例,用于存储依赖(Watcher)。

  2. 渲染阶段
    • Vue 渲染组件时,会访问 data 中的属性。
    • 访问属性时,getter 被触发,当前的 Watcher 被添加到 Dep 的订阅列表中。

  3. 数据变化阶段
    • 当数据发生变化时,setter 被触发。
    • setter 通知 DepDep 遍历所有订阅的 Watcher,并调用它们的 update 方法。

  4. 视图更新阶段
    • Watcher 的 update 方法被调用,通常会重新渲染组件,更新视图。


4. 示例代码:完整的响应式系统

以下是一个简化的示例,展示了 Vue 2 响应式系统的核心部分:

// 依赖收集器
class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }

  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

// Watcher 类
class Watcher {
  constructor(vm, key, updateCallback) {
    this.vm = vm;
    this.key = key;
    this.updateCallback = updateCallback;
    Dep.target = this; // 设置当前 Watcher
    this.value = vm[key]; // 访问属性,触发 getter,进行依赖收集
    Dep.target = null; // 重置
  }

  update() {
    const newValue = this.vm[this.key];
    this.updateCallback(newValue);
  }
}

// 定义响应式属性
function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        dep.notify();
      }
    }
  });
}

// 示例使用
const data = {};
defineReactive(data, 'message', 'Hello Vue!');

new Watcher(data, 'message', (newValue) => {
  console.log(`视图更新:${newValue}`);
});

console.log(data.message); // 输出:Hello Vue!
data.message = 'Hello World!'; // 输出:视图更新:Hello World!
data.message = 'Hello Vue 3!'; // 输出:视图更新:Hello Vue 3!

解释:
Dep 类用于管理依赖(Watcher)。
Watcher 类表示一个观察者,当依赖的数据变化时,会执行回调函数。
defineReactive 函数使用 Object.defineProperty 将对象的属性转换为响应式属性。
• 当 data.message 被访问时,依赖被收集;当被修改时,通知所有 Watcher 进行更新。


5. Vue 2 响应式系统的局限性

虽然 Object.defineProperty 能够实现基本的响应式功能,但它也有一些局限性:

  1. 无法监听对象属性的新增和删除
    • Vue 2 只能在初始化时劫持已有的属性。如果后续动态添加或删除属性,Vue 无法自动捕获这些变化。
    解决方法:使用 Vue.setVue.delete 方法。

  2. 无法监听数组的变化
    • 直接通过索引修改数组元素,或者修改数组的长度,Vue 无法检测到这些变化。
    解决方法:Vue 对数组的变异方法(如 pushpopsplice 等)进行了重写,使得这些方法能够触发视图更新。

  3. 性能开销
    • 对于大型数据结构,递归地进行响应式转换可能会带来一定的性能开销。


6. Vue 3 的改进

Vue 3 引入了 Proxy 来替代 Object.definePropertyProxy 能够更灵活地拦截对象的操作,包括属性的新增、删除、数组的变化等,从而解决了 Vue 2 中的一些局限性,并提升了性能。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容