反应性系统是现代前端框架的关键部分之一。它们是使应用程序高度互动、动态和响应的魔杖。了解什么是反应性系统,以及如何在实践中应用它是每个网络开发者的关键技能。
反应性系统是一种自动保持数据源(模型)与数据表示层(视图)同步的机制。每当模型发生变化时,视图就会被重新渲染以反映这些变化。
让我们以一个简单的Markdown编辑器为例。它通常有两个面板:一个用于编写Markdown代码(修改底层模型),另一个用于预览编译后的HTML(显示更新后的视图)。当你在书写窗格中写东西时,它就会立即自动在预览窗格中预览。当然,这只是一个简单的例子。通常情况下,事情要复杂得多。
在许多情况下,我们想要显示的数据依赖于其他一些数据。在这种情况下,依赖关系被跟踪,数据也相应地被更新。例如,假设我们有一个fullName 属性,它依赖于firstName 和lastName 属性。当它的任何一个依赖关系被修改时,fullName 属性会被自动重新评估,结果会显示在视图中。
现在我们已经确定了什么是反应性,现在是时候学习新的Vue 3反应性如何工作,以及我们如何在实践中使用它。但在这之前,我们先来看看旧的Vue 2反应性及其注意事项。
对Vue 2反应性的简要探索
Vue 2的反应性或多或少是 "隐藏的"。无论我们把什么放在data 对象中,Vue都会隐含地使其具有反应性。一方面,这使开发者的工作更容易,但另一方面,它导致了更少的灵活性。
在幕后,Vue 2使用ES5Object.defineProperty()来将data 对象的所有属性转换为getters和setters。对于每个组件实例,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 对象)和处理程序对象,它定义了哪些操作将被拦截(get 和set 操作)。在handler 对象中,我们使用get 和set 陷阱来跟踪一个属性被读取和一个属性被修改/添加。我们设置控制台语句,以确保这些方法正常工作。
get 和set 陷阱接受以下参数。
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。