Vue 3中新的反应性系统(附实例)

94 阅读8分钟

反应性系统是现代前端框架的关键部分之一。它们是使应用程序高度互动、动态和响应的魔杖。了解什么是反应性系统,以及如何在实践中应用它是每个网络开发者的关键技能。

反应性系统是一种自动保持数据源(模型)与数据表示层(视图)同步的机制。每当模型发生变化时,视图就会被重新渲染以反映这些变化。

让我们以一个简单的Markdown编辑器为例。它通常有两个面板:一个用于编写Markdown代码(修改底层模型),另一个用于预览编译后的HTML(显示更新后的视图)。当你在书写窗格中写东西时,它就会立即自动在预览窗格中预览。当然,这只是一个简单的例子。通常情况下,事情要复杂得多。

在许多情况下,我们想要显示的数据依赖于其他一些数据。在这种情况下,依赖关系被跟踪,数据也相应地被更新。例如,假设我们有一个fullName 属性,它依赖于firstNamelastName 属性。当它的任何一个依赖关系被修改时,fullName 属性会被自动重新评估,结果会显示在视图中。

现在我们已经确定了什么是反应性,现在是时候学习新的Vue 3反应性如何工作,以及我们如何在实践中使用它。但在这之前,我们先来看看旧的Vue 2反应性及其注意事项。

对Vue 2反应性的简要探索

Vue 2的反应性或多或少是 "隐藏的"。无论我们把什么放在data 对象中,Vue都会隐含地使其具有反应性。一方面,这使开发者的工作更容易,但另一方面,它导致了更少的灵活性。

在幕后,Vue 2使用ES5Object.defineProperty()来将data 对象的所有属性转换为getterssetters。对于每个组件实例,Vue都会创建一个依赖性观察者实例。在组件的渲染过程中收集/跟踪的任何属性都被观察者记录下来。之后,当一个依赖的设置器被触发时,观察者会被通知,组件会重新渲染并更新视图。这基本上就是所有魔法的运作方式。不幸的是,有一些注意事项。

变化检测的注意事项

由于Object.defineProperty() 的限制,有一些数据变化是Vue无法检测的。这些包括。

  • 向/从一个对象中添加/删除一个属性(比如obj.newKey = value )。
  • 通过索引设置数组项目(如arr[index] = newValue
  • 修改一个数组的长度(如arr.length = newLength )。

幸运的是,为了处理这些限制,Vue为我们提供了Vue.setAPI方法,它可以向一个反应式对象添加一个属性,确保新的属性也是反应式的,从而触发视图更新。

让我们在下面的例子中探讨一下上述情况。

<div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button @click="addAgeProperty">Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="item, index in activities" :key="index">
      {{ item }}
      <button @click="editActivity(index)">Edit</button>
    </li>
  </ul>
  <button @click="clearActivities">Clear the activities list</button>
</div>
const App = new Vue({
  el: '#app',
  data: {
    person: {
      name: "David"
    },
    activities: [
      "Reading books",
      "Listening music",
      "Watching TV"
    ]
  },
  methods: { 
    // 1. Add a new property to an object
    addAgeProperty() {
      this.person.age = 30
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        this.activities[index] = newValue
      }
    },
    // 3. Modifying the length of the array
    clearActivities() { 
      this.activities.length = 0 
    }
  }
});

这里有一个CodePen的例子

在上面的例子中,我们可以看到这三种方法都没有发挥作用。我们不能向person 对象添加一个新的属性。我们不能通过使用其索引来编辑activities 数组中的一个项目。我们也不能修改activities 数组的长度。

当然,这些情况有变通的方法,我们将在下一个例子中探讨。

const App = new Vue({
  el: '#app',
  data: {
    person: {
      name: "David"
    },
    activities: [
      "Reading books",
      "Listening music",
      "Watching TV"
    ]
  },
  methods: { 
    // 1. Adding a new property to the object
    addAgeProperty() {
      Vue.set(this.person, 'age', 30)
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        Vue.set(this.activities, index, newValue)
      }
    },
    // 3. Modifying the length of the array
    clearActivities() { 
      this.activities.splice(0)
    }
  }
});

这里有一个CodePen的例子

在这个例子中,我们使用Vue.set API方法向person 对象添加新的age 属性,并从活动数组中选择/修改一个特定的项目。在最后一种情况下,我们只是使用JavaScript内置的splice() 数组方法。

正如我们所看到的,这样做是可行的,但它有点笨拙,并导致代码库中的不一致。幸运的是,在Vue 3中,这个问题已经解决了。让我们在下面的例子中看看这个魔法的作用。

const App = {
  data() {
    return {
      person: {
        name: "David"
      },
      activities: [
        "Reading books",
        "Listening music",
        "Watching TV"
      ]
    }
  },
  methods: { 
    // 1. Adding a new property to the object
    addAgeProperty() {
      this.person.age = 30
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        this.activities[index] = newValue
      }
    },
    // 3. Modifying the length of the array
    clearActivities() { 
      this.activities.length = 0 
    }
  }
}

Vue.createApp(App).mount('#app')

这里有一个CodePen的例子

在这个使用Vue 3的例子中,我们恢复了第一个例子中使用的内置JavaScript功能,现在所有的方法都能正常工作。

在Vue 2.6中,引入了Vue.obsable()API方法。它在某种程度上暴露了反应性系统,允许开发者明确地使对象具有反应性。实际上,这与Vue内部用来包装data 对象的方法完全相同,对于为简单的场景创建一个最小的、跨组件的状态存储是非常有用的。但是,尽管它很有用,这种单一的方法不能与Vue 3中完整的、功能丰富的反应性API的力量和灵活性相比。我们将在接下来的章节中看到原因。

注意:由于Object.defineProperty() ,Vue 2不支持IE8及以下版本,这是一个仅适用于ES5且不可闪的功能。

Vue 3反应性如何工作

Vue 3中的反应性系统被完全重写,以便利用ES6代理反射API的优势。新版本暴露了一个功能丰富的反应性API,使该系统比以前更加灵活和强大。

代理API允许开发者在目标对象上拦截和修改低级别的对象操作。代理是一个对象(称为目标)的克隆/包装器,并提供特殊的功能(称为陷阱),它响应特定的操作,并覆盖JavaScript对象的内置行为。如果你仍然需要使用默认的行为,你可以使用相应的反射API,其方法,正如其名称所示,反映了代理API的方法。让我们来探讨一个例子,看看这些API是如何在Vue 3中使用的。

let person = {
  name: "David",
  age: 27
};

const handler = {
  get(target, property, receiver) {
    // track(target, property)
    console.log(property) // output: name
    return Reflect.get(target, property, receiver)
  },
  set(target, property, value, receiver) {
    // trigger(target, property)
    console.log(`${property}: ${value}`) // output: "age: 30" and "hobby: Programming"
    return Reflect.set(target, property, value, receiver)
  }
}

let proxy = new Proxy(person, handler);   

console.log(person)

// get (reading a property value)
console.log(proxy.name)  // output: David

// set (writing to a property)
proxy.age = 30;

// set (creating a new property)
proxy.hobby = "Programming";

console.log(person) 

这里有一个CodePen的例子

要创建一个新的代理,我们使用new Proxy(target, handler) 构造函数。它需要两个参数:目标对象(person 对象)和处理程序对象,它定义了哪些操作将被拦截(getset 操作)。在handler 对象中,我们使用getset 陷阱来跟踪一个属性被读取和一个属性被修改/添加。我们设置控制台语句,以确保这些方法正常工作。

getset 陷阱接受以下参数。

  • target:被代理包裹的目标对象
  • property:属性名称
  • value属性值(这个参数只用于设置操作)。
  • receiver:发生操作的对象(通常是代理)。

Reflect API方法接受的参数与它们相应的代理方法相同。它们被用来实现给定操作的默认行为,对于get trap来说,它返回属性名称,对于set trap来说,如果属性被设置,则返回true ,如果没有,则返回false

被评论的track()trigger() 函数是Vue特有的,用于跟踪一个属性被读取和一个属性被修改/添加的时间。结果是,Vue重新运行使用该属性的代码。

在这个例子的最后部分,我们使用一个控制台语句来输出原始的person 对象。然后我们用另一条语句来读取proxy 对象的属性name 。接下来,我们修改age 属性并创建一个新的hobby 属性。最后,我们再次输出person 对象,看看它是否已经被正确地更新。

这就是Vue 3的反应性的工作方式,简而言之。当然,真正的实现要复杂得多,但希望上面介绍的例子足以让你掌握主要思想。

当你使用Vue 3的反应性时,还有一些注意事项。

  • 它只适用于支持ES6+的浏览器
  • 反应式代理并不等同于原始对象

继续阅读:了解Vue 3中新的反应性系统,请访问SitePoint