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 中, propscomputeddata()默认情况下都是响应式的,但创建此类组件时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函数接受两个参数: propscontext 。 这个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方法创建了一个响应式对象,具有ToyotaHonda属性。 我们还利用toRefHonda创建了一个反应变量。 从上面的示例中,我们可以看到,当我们使用响应式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