Reactivity systems是现代前端框架的关键部分之一。它们是使应用程序具有高度交互性,动态性和响应能力的魔杖。对于每个Web开发人员而言,了解什么是反应性系统以及如何在实践中应用它是一项至关重要的技能。
Reactivity systems是一种自动使数据源(模型)与数据表示(视图)层保持同步的机制。每次模型更改时,都会重新渲染视图以反映更改。
让我们以一个简单的Markdown编辑器为例。它通常有两个窗格:一个窗格用于编写Markdown代码(用于修改基础模型),另一个窗格用于预览已编译的HTML(显示已更新的视图)。当您在书写窗格中写东西时,它会立即在预览窗格中自动预览。当然,这只是一个简单的例子。通常情况要复杂得多。
在许多情况下,我们要显示的数据取决于其他一些数据。在这种情况下,将跟踪相关性,并相应地更新数据。例如,假设我们有一个fullName
属性,该属性取决于firstName
和lastName
属性。修改其任何依赖项后,将fullName
自动重新评估该属性,并在视图中显示结果。
现在我们已经确定了什么是反应性,现在该学习新的Vue 3反应性如何工作以及如何在实践中使用它了。但是在执行此操作之前,我们将快速浏览一下旧的Vue 2反应性及其注意事项。
Vue 2反应性简介
Vue 2中的反应性或多或少被"隐藏"。无论我们放置在data
对象中的是什么,Vue都会使其隐式反应。一方面,这使开发人员的工作更加轻松,但另一方面,这导致灵活性降低。
在幕后,Vue 2使用ES5 Object.defineProperty()将data
对象的所有属性转换为getter和setter。对于每个组件实例,Vue创建一个依赖关系观察程序实例。观察者会记录在组件渲染期间作为依赖项收集/跟踪的任何属性。稍后,当触发依赖项的设置器时,将通知观察者,并且组件将重新渲染并更新视图。基本上,这就是所有魔术的工作方式。不幸的是,有一些警告。
变更检测警告 {#changedetectioncaveats}
由于的限制Object.defineProperty()
,Vue无法检测到某些数据更改。这些包括:
-
向对象添加属性或从对象移除属性(例如
obj.newKey = value
) -
按索引设置数组项(例如
arr[index] = newValue
) -
修改数组的长度(例如
arr.length = newLength
)
幸运的是,为了解决这些局限性,Vue为我们提供了Vue.set API方法,该方法向响应对象添加了一个属性,确保新属性也是响应性的,从而触发了视图更新。
让我们在以下示例中探讨上述情况:
<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
}
}});
在上面的示例中,我们可以看到这三种方法都不起作用。我们无法向该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)
}
}});
在此示例中,我们使用Vue.set
API方法将新age
属性添加到person
对象,并从活动数组中选择/修改特定项目。在最后一种情况下,我们仅使用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')
在这个使用Vue 3的示例中,我们恢复到第一个示例中使用的内置JavaScript功能,现在所有方法都可以正常工作。
在Vue 2.6中,引入了Vue.observable() API方法。它在某种程度上公开了反应性系统,使开发人员可以明确地使对象具有反应性。实际上,这是Vue内部使用的用于包装data
对象的完全相同的方法,对于为简单场景创建最小的跨组件状态存储很有用。但是,尽管它有用,但无法与Vue 3随附的完整,功能丰富的反应性API的功能和灵活性相提并论。我们将在下一节中说明原因。
注意:由于Object.defineProperty()
是仅限ES5且不可调整的功能,因此Vue 2不支持IE8及以下版本。
Vue 3反应性如何工作
为了充分利用ES6 Proxy and Reflect API ,Vue 3中的反应性系统已被完全重写。新版本公开了功能丰富的反应性API,该API使系统比以前更加灵活和强大。
代理API允许开发人员拦截和修改目标对象上的低级对象操作。代理是对象(称为目标)的克隆/包装,并提供特殊功能(称为陷阱),这些功能响应特定的操作并覆盖JavaScript对象的内置行为。如果仍然需要使用默认行为,则可以使用相应的Reflection API,其名称顾名思义就是反映Proxy API的方法。让我们探索一个示例,以了解如何在Vue 3中使用这些API:
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)
要创建一个新的代理,我们使用new Proxy(target, handler)
构造函数。它带有两个参数:目标对象(person
对象)和处理程序对象,该对象定义将拦截哪些操作(get
和set
操作)。在handler
对象中,我们使用get
和set
陷阱来跟踪何时读取属性以及何时修改/添加属性。我们设置控制台语句以确保方法正确运行。
在get
和set
陷阱采用下列参数:
-
target
:代理包装的目标对象 -
property
:属性名称 -
value
:属性值(此参数仅用于设置操作) -
receiver
:进行操作的对象(通常是代理)
Reflect API方法与其相应的代理方法接受相同的参数。它们用于实现给定操作的默认行为,即对于get
陷阱返回属性名称,对于set
陷阱返回true
属性是否设置false
。
注释track()
和trigger()
函数特定于Vue,用于跟踪何时读取属性以及何时修改/添加属性。结果,Vue重新运行使用该属性的代码。
在示例的最后一部分,我们使用控制台语句输出原始person
对象。然后,我们用另一份声明中读取属性name
中的proxy
对象。接下来,我们修改age
属性并创建一个新hobby
属性。最后,我们person
再次输出该对象以查看它是否已正确更新。
简而言之,这就是Vue 3反应性的工作方式。当然,实际的实现方式要复杂得多,但是希望上面提供的示例足以让您掌握主要思想。
使用Vue 3反应性时,还有一些注意事项:
-
它仅适用于支持ES6 +的浏览器
-
反应式代理不等于原始对象
探索Vue 3反应性API
最后,我们了解Vue 3反应性API本身。在以下各节中,我们将探索分为逻辑组的API方法。我将方法归为一组,因为我认为以这种方式介绍方法会更容易记住。让我们从基础开始。
基本方法
第一组包括控制数据反应性的最基本方法:
-
ref
接受一个原始值或一个普通对象,然后返回一个反应性且可变的ref对象。ref对象只有一个value
指向原始值或纯对象的属性。 -
reactive
接收一个对象并返回该对象的反应性副本。转换很深,并且会影响所有嵌套属性。 -
readonly
接受一个ref或一个对象(普通或反应式),并将一个只读对象返回给原始对象。转换很深,并且会影响所有嵌套属性。 -
markRaw
返回对象本身,并防止将其转换为代理对象。
现在让我们看看这些方法的实际作用:
<h1>Hello, Vue 3 Reactivity API! :)</h1>
<hr>
<p><strong>Counter:</strong> {{ counter }}</p>
<button @click="counter++">+ Increment counter</button>
<br><br>
<button @click="counter--">- Decrement counter</button>
<hr>
<h3>Hello! My name is <mark>{{ person.name }}</mark>. I'm <mark>{{ person.age }}</mark> years old.</h3>
<p>Edit person's name
<input v-model="person.name" placeholder="name" /> and age
<input v-model="person.age" placeholder="age" />
</p>
<hr>
<p><strong>PI:</strong> {{ math.PI }}</p>
<button @click="math.PI = 6.28">Double PI</button> <span>(The console output after the button is clicked: <em>"Set operation on key 'PI' failed: target is readonly."</em>)</span>
<hr>
<h3>Alphabet Numbers</h3>
<table>
<tr>
<th>Letter</th>
<th>Number</th>
</tr>
<tr v-for="(value, key) in alphabetNumbers">
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
</table>
<br>
<button @click="alphabetNumbers.B = 3">Change the value of B to 3</button><span> (Actually the letter B <em>is</em> changed to number 3 - <button @click="showValue">Show the value of B</button>, but it's <em>not</em> tracked by Vue.)</span>
import { ref, reactive, readonly, markRaw, isRef, isReactive, isReadonly, isProxy, onMounted } from 'vue';export default {
setup () {
const counter = ref(0)
const person = reactive({
name: 'David',
age: 36
})
const math = readonly({
PI: 3.14
})
const alphabetNumbers = markRaw({
A: 1,
B: 2,
C: 3
})
const showValue = () => {
alert(`The value of B is ${alphabetNumbers.B}`)
}
onMounted(() => {
console.log(isRef(counter)) // true
console.log(isReactive(person)) // true
console.log(isReadonly(math)) // true
console.log(isProxy(alphabetNumbers)) // false
})
return {
counter,
person,
math,
alphabetNumbers,
showValue
}
}};
在此示例中,我们探索了四种基本反应方法的使用。
首先,我们创建一个counter
ref对象,其值为0
。然后,在视图中,我们放置了两个按钮,用于增加和减少计数器的值。当使用这些按钮时,我们看到计数器实际上是无功的。
其次,我们创建一个person
反应对象。然后,在视图中,我们放置了两个输入控件,分别用于编辑一个人的name
和一个人的age
。当我们编辑人员的属性时,它们会立即更新。
第三,我们创建一个math
只读对象。然后,在视图中,我们设置一个按钮,用于将math
的PI
属性值加倍。但是,当我们单击按钮时,控制台中会显示一条错误消息,告诉我们该对象是只读的,并且我们无法修改其属性。
最后,我们创建一个alphabetNumbers
我们不想转换为代理的对象,并将其标记为raw。它包含所有字母及其相应的数字(为简便起见,此处仅使用前三个字母)。此顺序不太可能更改,因此我们有意使该对象保持简单,这对性能很有好处。我们将对象内容呈现在表中,并设置一个按钮,将B
property的值更改为3
。我们这样做是为了表明尽管可以修改对象,但这不会导致视图重新渲染。
markRaw
非常适合我们不需要反应的对象,例如一长串国家/地区代码,颜色名称及其对应的十六进制数字,等等。
最后,我们使用下一节中描述的类型检查方法来测试和确定我们创建的每个对象的类型。当应用最初呈现时,我们会使用onMounted()
生命周期钩子触发这些检查。
类型检查方法 {#typecheckmethods}
该组包含上述所有四个类型检查器:
-
isRef
检查值是否是引用对象。 -
isReactive
检查对象是是由reactive
创建还是readonly
通过包装由创建的另一个代理而创建的反应代理reactive
。 -
isReadonly
检查对象是否是由创建的只读代理readonly
。 -
isProxy
检查对象是否是由reactive
或创建的代理readonly
。
更多参考方法 {#morerefsmethods}
该组包含其他引用方法:
-
unref
返回引用的值。 -
triggerRef
执行与shallowRef
手动相关的任何效果。 -
customRef
创建具有自定义引用的显式控件,并对其依赖项跟踪进行显式控制,并更新触发。
浅层方法 {#shallowmethods}
该组中的方法是的"浅"的等同物ref
,reactivity
和readonly
:
-
shallowRef
创建一个ref,该ref仅跟踪其value
属性而不会使其值具有反应性。 -
shallowReactive
创建一个反应式代理,该代理仅跟踪其自身的属性(不包括嵌套对象)。 -
shallowReadonly
创建一个只读代理,该代理仅使自己的属性变为只读(不包括嵌套对象)。
通过检查以下示例,使这些方法更易于理解:
<h1>Hello, Vue 3 Reactivity API! :)</h1><hr><h2>Shallow Ref</h2><p><strong>Settings:</strong> {{settings}}
<br><br>
Width: <input v-model="settings.width" />
Height: <input v-model="settings.height" />
<br><br>
<button @click="settings = { width: 80, height: 80 }">
Change the settings' value
</button></p><hr><h2>Shallow Reactive</h2><p><strong>SettingsA:</strong> {{settingsA}} <br><br>
Width: <input v-model="settingsA.width" />
Height: <input v-model="settingsA.height" />
<br><br>
X: <input v-model="settingsA.coords.x" />
Y: <input v-model="settingsA.coords.y" /></p><hr><h2>Shallow Readonly</h2><p><strong>SettingsB:</strong> {{settingsB}}
<br><br>
Width: <input v-model="settingsB.width" />
Height: <input v-model="settingsB.height" />
<br><br>
<span>(The console output after trying to change the <strong>width</strong> or <strong>height</strong> is: <em>"Set operation on key 'width/height' failed: target is readonly."</em>)</span>
<br><br>
X: <input v-model="settingsB.coords.x" />
Y: <input v-model="settingsB.coords.y" /></p>
import {ref, shallowRef, shallowReactive, shallowReadonly, isRef, isReactive, isReadonly, onMounted } from 'vue';export default {
setup () {
const settings = shallowRef({
width: 100,
height: 60
})
const settingsA = shallowReactive({
width: 110,
height: 70,
coords: {
x: 10,
y: 20
}
})
const settingsB = shallowReadonly({
width: 120,
height: 80,
coords: {
x: 20,
y: 40
}
})
onMounted(() => {
console.log(isReactive(settings)) // false
console.log(isReactive(settingsA)) // true
console.log(isReactive(settingsA.coords)) // false
console.log(isReadonly(settingsB)) // true
console.log(isReadonly(settingsB.coords)) // false
})
return {
settings,
settingsA,
settingsB
}
}};
本示例从创建settings
浅引用对象开始。然后,在视图中,添加两个输入控件以编辑其width
和height
属性。但是,当我们尝试修改它们时,我们发现它们没有更新。为了解决这个问题,我们添加了一个按钮,该按钮可以更改整个对象及其所有属性。现在可以了。这是因为value
的内容(width
以及height
作为单个属性)不会转换为反应性对象,但value
仍会跟踪(整个对象)的变异。
接下来,我们创建一个settingsA
浅层反应式代理,其中包含width
和height
属性,以及一个coords
带有x
和y
属性的嵌套对象。然后,在视图中,我们为每个属性设置一个输入控件。修改width
和height
属性时,我们看到它们已经进行了反应性更新。但是,当我们尝试修改x
和y
属性时,我们发现它们没有被跟踪。
最后,我们创建一个settingsB
浅层只读对象,其属性与相同settingsA
。在这里,当我们尝试修改width
orheight
属性时,控制台中会显示一条错误消息,告诉我们该对象是只读的,我们无法修改其属性。另一方面,可以毫无问题地修改x
和y
属性。
coords
最后两个示例中的嵌套对象均不受转换的影响,并且保持不变。这意味着它可以自由修改,但Vue不会跟踪其任何修改。
转换方式 {#conversionmethods}
接下来的三种方法用于将代理转换为ref或普通对象:
-
toRef
为源反应对象上的属性创建一个引用。引用将反应性连接保持到其源属性。 -
toRefs
将反应对象转换为普通对象。普通对象的每个属性都是一个指向原始对象相应属性的ref。 -
toRaw
返回areactive
或readonly
proxy的原始对象。
在下面的示例中,让我们看看这些转换是如何工作的:
<h1>Hello, Vue 3 Reactivity API! :)</h1><hr><h3>Hello! My name is <mark>{{ person.name }}</mark>.
I'm <mark>{{ person.age }}</mark> years old.
My hobby is programming.</h3><hr><h2>To Ref</h2><p>
Name (ref): <input v-model="name" />
Person's name: <input v-model="person.name" /></p><hr><h2>To Refs</h2><p>
PersonDetails' age (ref): <input v-model="personDetails.age.value" />
Person's age: <input v-model="person.age" /></p><hr><h2>To Raw</h2><p>
<strong>RawPerson's hobby:</strong> {{rawPerson.hobby}} <br><br>
RawPerson's hobby: <input v-model="rawPerson.hobby" /></p>
import { reactive, toRef, toRefs, toRaw, isReactive, isRef, onMounted } from 'vue';export default {
setup () {
const person = reactive({
name: 'David',
age: 30,
hobby: 'programming'
})
const name = toRef(person, 'name')
const personDetails = toRefs(person)
const rawPerson = toRaw(person)
onMounted(() => {
console.log(isRef(name)) // true
console.log(isRef(personDetails.age)) // true
console.log(isReactive(rawPerson)) // false
})
return {
person,
name,
personDetails,
rawPerson
}
}};
在此示例中,我们首先创建一个基础person
反应对象,并将其用作源对象。
然后,我们将该人的name
财产转换为具有相同名称的ref。然后,在视图中,我们添加两个输入控件-一个用于name
引用,另一个用于该人的name
财产。当我们修改其中一个时,另一个也会相应地更新,因此它们之间的反应性连接得以保持。
接下来,我们将一个人的所有属性转换为personDetails
对象中包含的各个引用。然后,在视图中,我们再次添加两个输入控件以测试我们刚刚创建的引用之一。正如我们看到的那样,personDetailsage
与人的age
属性完全同步,就像前面的示例一样。
最后,我们将person
反应性对象转换为rawPerson
普通对象。然后,在视图中,我们添加一个输入控件以编辑rawPerson的hobby
属性。但是正如我们所看到的,Vue不会跟踪转换后的对象。
计算和监视方法 {#computedandwatchmethods}
最后一组方法用于计算复杂值并"窥探"某些值:
-
computed
以getter函数作为参数,并返回一个不变的反应式ref对象。 -
watchEffect
立即运行一个函数,并以反应方式跟踪其依赖关系,并在依赖关系更改时重新运行它。 -
watch
与Options APIthis.$watch
和相应的watch
选项完全等效。它监视特定的数据源,并在监视的源发生更改时在回调函数中施加副作用。
让我们考虑以下示例:
<h1>Hello, Vue 3 Reactivity API! :)</h1>
<hr>
<h3>Hello! My name is <mark>{{ fullName }}</mark>.</h3>
<p>
First Name: <input v-model="firstName" />
Last Name: <input v-model="lastName" /></p>
<hr>
<strong>Volume:</strong> {{volume}}<br><br>
<button @click="volume++">+ Increment volume</button><hr>
<strong>State:</strong> {{state}}<br><br>
<button @click="state = state == 'playing' ? 'paused' : 'playing' ">Change state</button>
import { ref, computed, watch, watchEffect } from 'vue';export default {
setup () {
// computed
const firstName = ref('David')
const lastName = ref('Wilson')
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
// watchEffect
const volume = ref(0)
watchEffect(() => {
if (volume.value != 0 && volume.value % 3 == 0) {
alert("The volume's value can be divided into 3")
}
})
// watch
const state = ref('playing')
watch(state, (newValue, oldValue) =>
alert(`The state was changed from ${oldValue} to ${newValue}`)
)
return {
firstName,
lastName,
fullName,
volume,
state
}
}};
在此示例中,我们创建了一个fullName
计算变量,该变量的计算基于firstName
和lastName
。然后,在视图中,我们添加了两个输入控件,用于编辑全名的两个部分。正如我们所看到的,当我们修改任何一部分时,fullName
都会重新计算并更新结果。
接下来,我们创建一个volume
ref并为其设置观看效果。每次volume
修改后,效果将运行回调函数。为了证明这一点,我们在视图中添加了一个按钮,该按钮将音量增加一倍。我们在回调函数中设置一个条件,以测试该卷的值是否可以分为3,并在该卷返回true时显示一条警报消息。在启动应用程序并设置音量值后,效果将运行一次,然后在每次修改音量值时都将再次运行效果。
最后,我们创建一个state
ref并设置一个watch函数来跟踪它的更改。一旦state
改变,回调函数将被执行。在此示例中,我们添加了一个按钮,用于在playing
和之间切换状态paused
。每次发生这种情况时,都会显示一条警报消息。
watchEffect
并且watch
看起来几乎相同的功能方面,但他们有一些明显的不同:
-
watchEffect
将回调函数中包含的所有反应性属性视为依赖项。因此,如果回调包含三个属性,则会隐式跟踪所有属性的更改。 -
watch
仅跟踪我们作为回调参数包含的属性。此外,它还提供了watched属性的先前值和当前值。
如您所见,Vue 3反应性API为各种用例提供了许多方法。API很大,在本教程中,我仅探讨了基础知识。有关更深入的探索,详细信息和边缘案例,请访问Reactivity API文档。
结论
在本文中,我们介绍了什么是反应性系统以及它在Vue 2和Vue 3中的实现方式。我们看到Vue 2具有一些缺陷,这些缺陷已在Vue 3中成功解决。Vue 3反应性是基于现代JavaScript的完整重写。特征。让我们总结一下它的优缺点。
好处:
-
它可以用作独立程序包。例如,您可以将其与React一起使用。
-
凭借其功能丰富的API,它提供了更多的灵活性和功能。
-
它支持更多的数据结构(Map,WeakMap,Set,WeakSet)。
-
它具有更好的性能。仅使所需的数据具有反应性。
-
解决了Vue 2中的数据操作警告。
缺点:
-
它仅适用于支持ES6 +的浏览器。
-
在身份比较(
===
)方面,反应式代理不等于原始对象。 -
与Vue 2"自动"反应性相比,它需要更多的代码。
最重要的是,Vue 3反应性是一个灵活而强大的系统,Vue和非Vue开发人员都可以使用。无论您遇到什么情况,只要抓住它并开始构建很棒的东西即可。