返回

Vue 原理解析:任何人都能看懂的响应式原理和数据劫持原理,实现一个 $set

前端

在 Vue.js 应用开发中,我们经常会遇到数据变化后视图没有及时更新的情况,这通常是因为我们操作的数据并非响应式的。Vue 的响应式系统是其核心特性之一,它能追踪数据的变化并自动更新视图,但其工作机制也存在一些限制。例如,直接修改数组的索引或给对象添加新的属性,并不会触发 Vue 的响应式更新。

为了解决这个问题,Vue 提供了一些内置方法,例如 Vue.setthis.$set,以及数组的变异方法,如 pushpopsplice 等。这些方法能够确保我们对数据进行的操作能够被 Vue 的响应式系统追踪到,从而实现视图的自动更新。

一、Vue 响应式原理浅析

Vue 的响应式系统依赖于 Object.defineProperty() 这个 JavaScript API。简单来说,Vue 会遍历 data 中的所有属性,利用 Object.defineProperty() 将它们转换为 getter 和 setter。当我们读取某个属性时,会触发对应的 getter;当我们修改某个属性时,会触发对应的 setter。

在 setter 中,Vue 会通知所有依赖这个属性的 Watcher 进行更新。Watcher 是 Vue 中的一个观察者,它负责将数据变化反映到视图上。

二、为什么直接修改数组索引或添加属性无法触发更新

由于 JavaScript 语言的限制,Object.defineProperty() 无法追踪到数组索引的变化以及对象新增属性的情况。因此,当我们直接修改数组索引或给对象添加新的属性时,Vue 的 setter 就不会被触发,自然也就无法通知 Watcher 进行更新。

三、解决方案:Vue.set / this.$set 和数组变异方法

为了解决这个问题,Vue 提供了以下几种解决方案:

1. Vue.set / this.$set

Vue.setthis.$set 的作用是手动触发 Vue 的响应式系统。它们接收三个参数:目标对象、属性名和属性值。当我们使用这两个方法修改数据时,Vue 会检查目标对象是否是响应式的,如果不是,则会将其转换为响应式对象,然后再进行属性设置。

例如,我们要给一个对象 obj 添加一个新的属性 age,并将其值设置为 20,可以使用以下代码:

Vue.set(obj, 'age', 20); 
// 或 
this.$set(obj, 'age', 20); // 在 Vue 实例内部

同样地,如果我们要修改数组 arr 的第 2 个元素的值,可以使用以下代码:

Vue.set(arr, 1, 'new value');
// 或 
this.$set(arr, 1, 'new value'); // 在 Vue 实例内部

2. 数组变异方法

Vue 重写了数组的一些方法,例如 pushpopshiftunshiftsplicesortreverse。当我们使用这些方法修改数组时,Vue 会自动触发响应式更新。

例如,我们要在数组 arr 的末尾添加一个元素,可以使用以下代码:

arr.push('new element');

四、示例演示

下面我们通过一个简单的示例来演示如何使用 Vue.set 和数组变异方法来解决数据更新问题。

<template>
  <div>
    <p>对象属性:{{ obj.name }} - {{ obj.age }}</p>
    <button @click="addAge">添加年龄</button>

    <p>数组元素:{{ arr.join(', ') }}</p>
    <button @click="addItem">添加元素</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {
        name: '张三'
      },
      arr: [1, 2, 3]
    };
  },
  methods: {
    addAge() {
      // this.obj.age = 20; // 这种方式无法触发更新
      this.$set(this.obj, 'age', 20); 
    },
    addItem() {
      this.arr.push(4);
    }
  }
};
</script>

在这个例子中,我们初始时只给 obj 对象设置了 name 属性,没有设置 age 属性。当我们点击 "添加年龄" 按钮时,会调用 addAge 方法,使用 this.$setobj 对象添加 age 属性,并将其值设置为 20。此时,视图中的 obj.age 会自动更新。

同样地,当我们点击 "添加元素" 按钮时,会调用 addItem 方法,使用 push 方法在 arr 数组的末尾添加一个元素。此时,视图中的数组元素列表也会自动更新。

五、常见问题解答

1. Vue.set 和 this.$set 有什么区别?

Vue.set 是一个全局方法,可以在任何地方使用;this.$set 是 Vue 实例上的一个方法,只能在 Vue 实例内部使用。它们的功能是完全一样的。

2. 为什么 Vue 要重写数组的某些方法?

因为 JavaScript 语言的限制,Object.defineProperty() 无法追踪到数组索引的变化。为了解决这个问题,Vue 重写了数组的一些方法,例如 pushpopsplice 等,在这些方法内部手动触发响应式更新。

3. 除了 Vue.set 和数组变异方法,还有其他方法可以触发响应式更新吗?

是的,还可以使用 this.$forceUpdate() 方法强制更新组件。但是,这种方法会重新渲染整个组件,性能开销较大,不建议频繁使用。

4. 如何判断一个对象是否是响应式的?

可以使用 Vue.util.isReactive(obj) 方法来判断一个对象是否是响应式的。

5. 如何将一个普通对象转换为响应式对象?

可以使用 Vue.observable(obj) 方法将一个普通对象转换为响应式对象。这个方法会返回一个新的响应式对象,原对象不会被修改.