Vue 中的反应性
已发表: 2022-03-10在本文中,我们将研究 Vue 中的反应性,它是如何工作的,以及我们如何使用新创建的方法和函数来创建反应性变量。 本文面向对 Vue 2.x 工作原理有深入了解并希望熟悉新的 Vue 3 的开发人员。
我们将构建一个简单的应用程序来更好地理解这个主题。 这个应用程序的代码可以在 GitHub 上找到。
默认情况下, JavaScript 不是响应式的。 这意味着如果我们创建变量boy
并在应用程序的 A 部分中引用它,然后继续修改 B 部分中的boy
,则 A 部分将不会更新为boy
的新值。
let framework = 'Vue'; let sentence = `${framework} is awesome`; console.log(sentence) // logs "Vue is awesome" framework = 'React'; console.log(sentence) //should log "React is awesome" if 'sentence' is reactive.
上面的代码片段是 JavaScript 非响应式特性的完美示例——因此,为什么更改没有反映在sentence
变量中。
在 Vue 2.x 中, props
、 computed
和data()
默认情况下都是响应式的,但创建此类组件时data
中不存在的属性除外。 这意味着当一个组件被注入到 DOM 中时,只有组件data
对象中的现有属性会在这些属性发生变化时导致组件更新。
在内部,Vue 3 使用Proxy
对象(ECMAScript 6 功能)来确保这些属性是反应性的,但它仍然提供使用 Vue 2 中的Object.defineProperty
的选项来支持 Internet Explorer(ECMAScript 5)。 此方法直接在对象上定义新属性,或修改对象上的现有属性,并返回该对象。
乍一看,由于我们大多数人已经知道反应性在 Vue 中并不新鲜,因此似乎没有必要使用这些属性,但是当您处理具有多个可重用函数的大型应用程序时,Options API 有其局限性应用程序的一部分。 为此,引入了新的 Composition API 以帮助抽象逻辑,以使代码库更易于阅读和维护。 此外,我们现在可以使用任何新属性和方法轻松地使任何变量反应,而不管其数据类型如何。
当我们使用setup
选项作为组合 API 的入口点时, data
对象、 computed
属性和methods
是不可访问的,因为在执行setup
时组件实例尚未创建。 这使得无法利用setup
中任何这些功能中的内置反应性。 在本教程中,我们将了解我们可以做到这一点的所有方法。
反应式方法
根据文档,在 Vue 2.6 中等效于Vue.observable()
的reactive
方法在我们尝试创建一个所有属性都是响应式的对象(例如 Options 中的data
对象)时很有用API)。 在底层,Options API 中的data
对象使用此方法来使其中的所有属性都具有反应性。
但是我们可以像这样创建自己的响应式对象:
import { reactive } from 'vue' // reactive state let user = reactive({ "id": 1, "name": "Leanne Graham", "username": "Bret", "email": "[email protected]", "address": { "street": "Kulas Light", "suite": "Apt. 556", "city": "Gwenborough", "zipcode": "92998-3874", "geo": { "lat": "-37.3159", "lng": "81.1496" } }, "phone": "1-770-736-8031 x56442", "website": "hildegard.org", "company": { "name": "Romaguera-Crona", "catchPhrase": "Multi-layered client-server neural-net", "bs": "harness real-time e-markets" }, "cars": { "number": 0 } })
在这里,我们从 Vue 导入了reactive
方法,然后我们通过将其值作为参数传递给该函数来声明我们的user
变量。 在这样做的过程中,我们使user
反应,因此,如果我们在模板中使用user
并且如果该对象的对象或属性发生变化,那么该值将在该模板中自动更新。
ref
正如我们有一种使对象具有反应性的方法一样,我们也需要一种方法来使其他独立的原始值(字符串、布尔值、未定义值、数字等)和数组具有反应性。 在开发过程中,我们将使用这些其他数据类型,同时还需要它们具有反应性。 我们可能会想到的第一种方法是使用reactive
并传入我们想要使其成为响应式的变量的值。
import { reactive } from 'vue' const state = reactive({ users: [], });
因为reactive
有很深的响应式转换,所以user
作为属性也会是响应式的,从而达到我们的目的; 因此, user
总是会在此类应用程序的模板中使用它的任何地方进行更新。 但是使用ref
属性,我们可以通过将该变量的值传递给ref
来使任何具有任何数据类型的变量都具有反应性。 此方法也适用于对象,但它比使用reactive
方法时将对象嵌套更深一层。
let property = { rooms: '4 rooms', garage: true, swimmingPool: false } let reactiveProperty = ref(property) console.log(reactiveProperty) // prints { // value: {rooms: "4 rooms", garage: true, swimmingPool: false} // }
在后台, ref
接受传递给它的这个参数并将其转换为一个具有value
键的对象。 这意味着,我们可以通过调用variable.value
来访问我们的变量,也可以通过同样的方式调用它来修改它的值。
import {ref} from 'vue' let age = ref(1) console.log(age.value) //prints 1 age.value++ console.log(age.value) //prints 2
有了这个,我们可以将ref
导入我们的组件并创建一个反应变量:
<template> <div class="home"> <form @click.prevent=""> <table> <tr> <th>Name</th> <th>Username</th> <th>email</th> <th>Edit Cars</th> <th>Cars</th> </tr> <tr v-for="user in users" :key="user.id"> <td>{{ user.name }}</td> <td>{{ user.username }}</td> <td>{{ user.email }}</td> <td> <input type="number" name="cars" v-model.number="user.cars.number" /> </td> <td> <cars-number :cars="user.cars" /> </td> </tr> </table> <p>Total number of cars: {{ getTotalCars }}</p> </form> </div> </template> <script> // @ is an alias to /src import carsNumber from "@/components/cars-number.vue"; import axios from "axios"; import { ref } from "vue"; export default { name: "Home", data() { return {}; }, setup() { let users = ref([]); const getUsers = async () => { let { data } = await axios({ url: "data.json", }); users.value = data; }; return { users, getUsers, }; }, components: { carsNumber, }, created() { this.getUsers(); }, computed: { getTotalCars() { let users = this.users; let totalCars = users.reduce(function(sum, elem) { return sum + elem.cars.number; }, 0); return totalCars; }, }; </script>
在这里,我们导入了ref
以便在我们的组件中创建一个响应式users
变量。 然后我们导入axios
以从public
文件夹中的 JSON 文件中获取数据,并导入我们稍后将创建的carsNumber
组件。 我们接下来要做的是使用ref
方法创建一个响应式users
变量,以便users
可以在我们的 JSON 文件的响应发生变化时进行更新。
我们还创建了一个getUser
函数,它使用 axios 从我们的 JSON 文件中获取users
数组,并将此请求中的值分配给users
变量。 最后,我们创建了一个计算属性,用于计算用户拥有的汽车总数,因为我们在模板部分对其进行了修改。
需要注意的是,当访问在模板部分或setup()
之外返回的ref
属性时,它们会自动浅展开。 这意味着作为对象的refs
仍然需要一个.value
才能被访问。 因为users
是一个数组,我们可以在getTotalCars
中简单地使用users
而不是users.value
。
在模板部分,我们展示了一个显示每个用户信息的表格,以及一个<cars-number />
组件。 该组件接受一个cars
道具,该道具显示在每个用户的行中,作为他们拥有的汽车数量。 每当用户对象中cars
的值发生变化时,此值就会更新,这正是我们使用 Options API 时data
对象或computed
属性的工作方式。
toRefs
当我们使用 Composition API 时, setup
函数接受两个参数: props
和context
。 这个props
从组件传递到setup()
,它使得从这个新 API 中访问组件具有的 props 成为可能。 这种方法特别有用,因为它允许在不失去反应性的情况下解构对象。
<template> <p>{{ cars.number }}</p> </template> <script> export default { props: { cars: { type: Object, required: true, }, gender: { type: String, required: true, }, }, setup(props) { console.log(props); // prints {gender: "female", cars: Proxy} }, }; </script> <style></style>
为了在 Composition API 中使用来自props
的对象的值,同时确保它保持其反应性,我们使用了toRefs
。 此方法接受一个响应式对象并将其转换为一个普通对象,其中原始响应式对象的每个属性都成为一个ref
。 这意味着cars
支撑...
cars: { number: 0 }
......现在会变成这样:
{ value: cars: { number: 0 }
有了这个,我们可以在设置 API 的任何部分中使用cars
,同时仍然保持其反应性。
setup(props) { let { cars } = toRefs(props); console.log(cars.value); // prints {number: 0} },
我们可以使用 Composition API 的watch
来观察这个新变量,并对这个变化做出我们可能想要的反应。
setup(props) { let { cars } = toRefs(props); watch( () => cars, (cars, prevCars) => { console.log("deep ", cars.value, prevCars.value); }, { deep: true } ); }
toRef
我们可能面临的另一个常见用例是传递一个值,该值不一定是对象,而是与ref
一起使用的数据类型之一(数组、数字、字符串、布尔值等)。 使用toRef
,我们可以从源反应对象创建反应属性(即ref
)。 这样做将确保该属性保持反应性,并在父源更改时更新。
const cars = reactive({ Toyota: 1, Honda: 0 }) const NumberOfHondas = toRef(state, 'Honda') NumberOfHondas.value++ console.log(state.Honda) // 1 state.Honda++ console.log(NumberOfHondas.value) // 2
在这里,我们使用reactive
方法创建了一个响应式对象,具有Toyota
和Honda
属性。 我们还利用toRef
从Honda
创建了一个反应变量。 从上面的示例中,我们可以看到,当我们使用响应式cars
对象或NumberOfHondas
更新Honda
时,两个实例中的值都会更新。
该方法与我们上面介绍的toRefs
方法相似但又如此不同,因为它保持与源的连接,可用于字符串、数组和数字。 与toRefs
不同,我们不需要担心在创建时它的源中是否存在该属性,因为如果在创建此ref
时该属性不存在而是返回null
,它仍然会被存储作为一个有效的属性,有一个watcher
的形式,所以当这个值改变时,这个使用toRef
创建的ref
也会被更新。
我们也可以使用这个方法从props
创建一个响应式属性。 看起来像这样:
<template> <p>{{ cars.number }}</p> </template> <script> import { watch, toRefs, toRef } from "vue"; export default { props: { cars: { type: Object, required: true, }, gender: { type: String, required: true, }, }, setup(props) { let { cars } = toRefs(props); let gender = toRef(props, "gender"); console.log(gender.value); watch( () => cars, (cars, prevCars) => { console.log("deep ", cars.value, prevCars.value); }, { deep: true } ); }, }; </script>
在这里,我们创建了一个基于从props
获得的gender
属性的ref
。 当我们想要对特定组件的 prop 执行额外操作时,这会派上用场。
结论
在本文中,我们使用 Vue 3 中新引入的一些方法和函数来了解 Vue 中的反应性如何工作。我们首先了解什么是反应性以及 Vue 如何在幕后使用Proxy
对象来实现这一点。 我们还研究了如何使用reactive
创建响应式对象以及如何使用ref
创建响应式属性。
最后,我们研究了如何将响应式对象转换为普通对象,每个对象的属性都是指向原始对象相应属性的ref
,并且我们看到了如何为响应式源对象上的属性创建ref
。
更多资源
- “代理”(对象),MDN Web Docs
- “反应性基础”,Vue.js
- “参考”,Vue.js
- “Lifecycle Hook Registration Inside
setup
”,Vue.js