带你深入了解一下vue中的响应式原理

2 阅读9分钟

前言

对于响应式原理,我们要先了解一下vue是一个 MVVM 结构的框架;也就是 数据层视图层数据-视图层;响应式的原理就是要实现当数据更新时,视图层也要相应的更新。基于响应式原理我们可以使数据驱动视图的实现变得简单而高效

响应式原理

对于Vue2中的响应式处理来说,他是基于js 的Object.defineProperty()方法的。它的原理主要是如下几步实现的:

  1. 数据劫持 :在Vue中,当你把一个普通js对象传给Vue实例作为data选项时,Vue将遍历此对象所有的属性,并使用 Object.defineProperty() 把这些属性全部转为 getter/setter。这样,Vue 能够追踪到属性的变化,并在属性被访问和修改时执行相应的操作
  2. 依赖追踪 :Vue内部维护了一个依赖收集的系统,每个响应式对象都有一个对应的依赖集合,当数据被访问时,会把当前的Watcher(观察者)记录下来。这样,当数据发生变化时,依赖于这个数据的所有Watcher都会被通知,进而更新相应的视图
  3. 派发更新 :当响应式数据发生变化时,Vue会遍历依赖集合,通知相关的Watcher更新视图

为什么需要有响应式呢

响应式是为了构建动态的、交互式的用户界面而设计的。如果没有响应式那么我们就需要手动手动去监听数据的变化然后更新视图,这样会导致性能上消耗增加,并且用户的体验也不会良好。而响应式使得页面能够在数据变化时实时更新,提供了更好的用户体验。用户可以看到页面的实时变化,而无需手动刷新页面

要如何实现响应式

对于响应式来说,它是基于 发布订阅模式数据劫持 来实现的,即:

  1. 发布-订阅者模式:Vue使用发布-订阅者模式来实现数据变动时的通知和更新
  2. 数据劫持:Vue通过Object.defineProperty对数据进行劫持

对于发布订阅模式我已经写过了一篇文章进行讲解了,大家感兴趣可以点击去看一下:面试官:能介绍一下你对发布订阅和单例模式的理解吗 - 掘金 (juejin.cn)

实现方法

// 定义Dep类,用于收集依赖和通知更新
class Dep {
  constructor() {
    this.subscribers = [];
  }
  // 添加订阅者
  addSubscriber(sub) {
    if (sub && typeof sub.update === 'function') {
      this.subscribers.push(sub);
    }
  }
  // 发布更新
  notify() {
    this.subscribers.forEach(sub => sub.update());
  }
}
// 定义Watcher类,用于订阅数据变化
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    Dep.target = this;
    this.vm[this.key]; // 触发 getter,收集依赖
    Dep.target = null;
  }
  // 更新视图
  update() {
    this.cb.call(this.vm, this.vm[this.key]);
  }
}
// 定义Observer类,用于将对象转为响应式对象
class Observer {
  constructor(data) {
    this.data = data;
    this.walk(data);
  }
  // 遍历对象属性,转为响应式
  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }
  // 定义响应式属性
  defineReactive(obj, key, value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) {
          dep.addSubscriber(Dep.target);
        }
        return value;
      },
      set(newValue) {
        if (value !== newValue) {
          value = newValue;
          dep.notify(); // 数据变化,通知更新
        }
      }
    });
  }
}
// Vue 类,用于创建 Vue 实例
class Vue {
  constructor(options) {
    this.options = options; 
    this._data = options.data;
    // 数据响应化
    new Observer(this._data);
    // 代理 data 到 Vue 实例上
    this.proxyData(this._data);
    // 创建 Watcher 实例,观察数据变化
    options.created && options.created.call(this);
  }
  // 代理 data 到 Vue 实例上
  proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this._data[key];
        },
        set(newValue) {
          this._data[key] = newValue;
        }
      });
    });
  }
}

可以从以上代码中看出:

  • Dep类用于收集依赖和通知更新,每个响应式数据都有一个对应的Dep实例
  • Watcher类用于订阅数据变化,当数据变化时,会触发对应Watcher的更新
  • Observer类用于将对象转为响应式对象,遍历对象属性,通过Object.defineProperty转为响应式属性
  • Vue类用于创建Vue实例,将data选项转为响应式对象,并创建对应的Watcher实例,观察数据变化

因此通过以上代码我们就实现了一个响应式原理了

vue3是如何实现响应式原理的呢

在 Vue 3 中,响应式原理采用了基于ES6 Proxy对象的方式来实现,相较于Vue2中基于Object.defineProperty 的方式,使用 Proxy 对象能够更灵活地拦截对对象的各种操作,从而实现更加高效和强大的响应式系统。

Object.defineProperty

通过以上内容我们可以看出,Object.defineProperty() 允许我们定义对象的属性,并且能够通过拦截访问和修改这些属性的行为,实现对属性的监控和响应。这使得当对象的属性发生变化时,能够触发相应的更新操作。但是我们要注意的是,它实现实现数据劫持不是进行数据代理,而是数据拦截的。就是说在数据被访问和修改时进行拦截,并在相应的时机触发更新操作,从而实现数据与视图的同步更新。

具体来说,在 Vue 的响应式系统中,当你把一个普通 JavaScript 对象传给 Vue 实例作为 data 选项时,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty() 方法把这些属性全部转为 getter/setter。这样一来,Vue 能够追踪到属性的变化,并在属性被访问和修改时执行相应的操作。

缺点

  1. 兼容性问题Object.defineProperty() 在 IE8 及更早版本的浏览器中不被支持,这限制了 Vue 在一些旧版本浏览器中的应用范围,需要额外的兼容处理
  2. 只能监听对象属性Object.defineProperty() 只能劫持对象的属性访问和修改操作,无法监听对象的新增属性和删除属性的操作。这导致在 Vue 中对于新增属性和删除属性的响应式处理需要额外的操作,例如需要使用 Vue.set() 或者 $set() 方法来添加新属性
  3. 无法监听数组变化Object.defineProperty() 无法直接监听数组的变化,因为数组的变化通常包括了数组的元素的添加、删除和重新排序等操作,这些操作不会触发数组的属性变化,从而无法被Object.defineProperty() 拦截。在 Vue 中对于数组的响应式处理是通过重写数组的一系列方法来实现的,如 push()pop()shift()unshift() 等,这种处理方式也增加了一定的复杂性
  4. 性能开销:对于大规模数据的响应式处理,Object.defineProperty() 可能会带来一定的性能开销。因为每个被劫持的属性都需要一个对应的 getter 和 setter 函数来进行拦截和更新操作,当属性较多时,可能会影响到整体性能

Proxy

对于Proxy 它是 ES6 中新增的一种代理机制,用于定义基本操作的自定义行为(例如属性查找、赋值、枚举、函数调用等)。它提供了一种强大而灵活的方式来监视并对对象的操作进行拦截和定制。因此在 Vue 3 中,我们可以使用Proxy 被用于实现数据的响应式处理

Proxy 的特点:

  1. 可定制行为:通过定义拦截器函数,可以对对象的各种操作进行定制,使得 Proxy 对象能够实现非常灵活的代理行为。
  2. 透明性:Proxy 对象与原对象具有相同的外观和行为,因此在代码中可以完全替代原对象,而不会影响到代码的其他部分。
  3. 非侵入性:Proxy 对象与原对象之间的代理关系是动态的,可以随时添加或移除代理行为,而不会影响到原对象。
  4. 更好的性能:与 Object.defineProperty() 相比,Proxy 的性能通常更好,特别是在处理大规模数据和数组变化时。

总的来说,Proxy 是一个强大而灵活的工具,能够对对象的操作进行拦截和定制,为我们提供了更好的数据处理和操作控制的能力。

结语

综上所述,我们可以得出以下结论:

  1. 理解 MVVM 结构:Vue 是一个典型的 MVVM 框架,它将数据层、视图层和数据-视图层进行了良好的分离,使得前端开发更加清晰和高效
  2. 掌握响应式原理:响应式原理是 Vue 实现数据驱动视图的关键,通过数据劫持和依赖追踪,实现了数据与视图之间的自动更新,为构建动态、交互式的用户界面提供了强大的支持
  3. 熟悉实现方式:在 Vue 2 中,响应式原理是基于Object.defineProperty()实现的,它具有一定的局限性和缺点,比如无法监听数组变化和兼容性问题。而在 Vue 3 中,采用了基于Proxy的方式,提供了更灵活、高效的响应式系统。
  4. 权衡利弊Object.defineProperty()Proxy各有优缺点,需要根据项目的具体需求和浏览器兼容性要求进行选择。Proxy 在性能和灵活性上有一定优势,但在一些旧版本浏览器中可能存在兼容性问题;而 Object.defineProperty() 则在兼容性较好的情况下实现了一定的数据劫持能力

两种实现方式的区别

  1. 灵活性:
  • Proxy 提供了更加灵活和强大的拦截能力,可以拦截对象的更多操作,包括属性的读取、赋值、删除、枚举等,以及数组的操作如 pushpopshiftunshift 等。而 Object.defineProperty() 只能劫持对象的属性访问和修改操作,无法直接监听数组的变化等
  1. 兼容性:
  • Proxy 在 ES6 中被引入,因此对于支持 ES6 的现代浏览器和环境来说,兼容性较好。但是在一些旧版本的浏览器中,如 IE11 及更早版本,Proxy 并不被支持。而 Object.defineProperty() 在较早的 ES5 中就已经存在,兼容性较好,但也存在一些兼容性问题,如无法监听数组变化和对新增属性的处理等
  1. 性能:
  • Proxy 相对于 Object.defineProperty() 在性能上可能会有所提升,特别是在处理大规模数据和数组变化时。Proxy 的拦截器函数在实现上更为底层,因此可能更加高效。而 Object.defineProperty() 的性能开销相对较大,特别是在属性较多时可能会影响到整体性能
  1. 监听对象的方式:
  • Proxy是通过创建一个目标对象的代理对象来实现监听的,可以直接监听整个对象,包括对象的属性新增、删除和修改等操作。而Object.defineProperty()是针对对象的每个属性进行劫持,无法直接监听对象的整体变化

总的来说,深入理解 Vue 的响应式原理和不同的实现方式,有助于我们更加深入地理解前端框架的底层原理,并能够更加灵活地应对各种开发场景和需求