一、计算属性
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如,有一个嵌套数组对象:我们想根据author
是否已经有一些书来显示不同的消息
<div id="computed-basics"> <p>Has published books:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span> </div> <script> Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } } }).mount("#computed-basics") </script>
此时,模板不再是简单的和声明性的。你必须先看一下它,然后才能意识到它执行的计算取决于 author.books
。如果要在模板中多次包含此计算,则问题会变得更糟。
所以,对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性。
<div id="computed-basics"> <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span> </div> <script> Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }, computed: { // 计算属性的 getter publishedBooksMessage() { // `this` 指向 vm 实例 return this.author.books.length > 0 ? 'Yes' : 'No' } } }).mount('#computed-basics') </script>
这里声明了一个计算属性 publishedBooksMessage
。
尝试更改应用程序 data
中 books
数组的值,你将看到 publishedBooksMessage
如何相应地更改。
你可以像普通属性一样将数据绑定到模板中的计算属性。Vue 知道 vm.publishedBookMessage
依赖于 vm.author.books
,因此当 vm.author.books
发生改变时,所有依赖 vm.publishedBookMessage
的绑定也会更新。
计算属性缓 vs 方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>{{ calculateBooksMessage() }}</p>
// 在组件中 methods: { calculateBooksMessage() { return this.author.books.length > 0 ? 'Yes' : 'No' } }
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 author.books 还没有发生改变,多次访问 publishedBookMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。 这也同样意味着下面的计算属性将不再更新,因为 Date.now () 不是响应式依赖:
computed: { now() { return Date.now() } }
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list
。如果没有缓存,我们将不可避免的多次执行 list
的 getter!如果你不希望有缓存,请用 method
来替代。
案例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.1.2/vue.global.js"></script> <style> body { font-size: 14px; } table, tr, th, td { border: 1px solid red; border-collapse: collapse; /* 合并边框 */ } th, td { width: 200px; text-align: center; /* 文本水平居中 */ height: 30px; line-height: 30px; } input { width: 80px; } .active { background-color: lightskyblue; } </style> </head> <body> <div id="app"> <table> <tr> <th>商品ID</th> <th>商品标题</th> <th>商品库存</th> <th>商品单价</th> <th>购买数量</th> <th>商品小计</th> </tr> <tr v-for="book in book_list"> <td>{{book.id}}</td> <td>《{{book.title}}》</td> <td>{{book.max_num}}</td> <td>{{book.price}}</td> <td> <button @click="sub(book)">-</button> <input type="text" v-model.number="book.num"> <button @click="add(book)">+</button> </td> <td>{{total(book)}}</td> </tr> <tr> <td colspan="4"></td> <td>总计</td> <td>{{calc}}</td> </tr> </table> </div> <script> Vue.createApp({ data() { return { book_list: [ {id: 10, title: "诛仙", price: 98.50, num: 1, max_num: 7,}, {id: 110, title: "元月", price: 68.50, num: 1, max_num: 5,}, {id: 30, title: "一月", price: 108.50, num: 1, max_num: 3,}, {id: 100, title: "二月", price: 78.50, num: 1, max_num: 10,}, ] } }, // 所谓的计算属性,就是新声明一个新的变量,这个变量是经过data里面的数据运算后的结果。 computed: { calc() { let ret = 0; this.book_list.forEach((book, key) => { ret += book.price * book.num; }); return ret.toFixed(2); } }, methods: { total(book) { return (book.price * book.num).toFixed(2); }, sub(book) { // 减少数量 if (book.num > 1) { book.num -= 1; } }, add(book) { // 增加数量 if (book.num < book.max_num) { book.num += 1; } } } }).mount('#app') </script> </body> </html>
二、侦听属性(监听属性)
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
侦听属性是一个对象,它的键是要监听的对象或者变量,值一般是函数,当侦听的data数据发生变化时,会自定执行的对应函数,这个函数在被调用时,vue会传入两个形参,第一个是变化前的数据值,第二个是变化后的数据值。
案例1:
<div id="computed-basics"> <p>{{num}}</p> <p><input type="text" v-model="num"></p> </div> <script> Vue.createApp({ data() { return { num: 10 } }, watch: { num: function (newval, oldval) { //num发生变化的时候,要执行的代码 console.log(`num已经从${oldval}变成${newval}`); } } }).mount('#computed-basics') </script>
案例2:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.1.2/vue.global.js"></script> <style> input[type=password] { outline-color: red; } input.is_pass { outline-color: green; } input.is_fail { outline-color: red; } </style> </head> <body> <div id="app"> <p> <input type="password" :class="tips" v-model="password"> </p> </div> <script> Vue.createApp({ data() { return { tips: "is_fail", password:"" } }, watch: { password() { if (this.password.length > 6 && this.password.length < 16) { this.tips = "is_pass" } else { this.tips = "is_fail"; } } }, }).mount('#app') </script> </body> </html>