看下面的例子,假设有一个嵌套数组对象author:
Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } } })
需求是,如果该作者出过书,则显示yes,否则显示no。我们可以在HTML的span标签中插入JS表达式来实现:
<div id="computed-basics"> <p>Has published books:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span> </div>
这个三元表达式不是简单的和声明性的(尽管相对写个方法,写个类,这起码还是一条语句。)
我们必须先看一下它,然后才能意识到它执行的操作取决于 author.books
。
如果要在模板中多次进行这个判断,问题会变得更糟。
Vue的设计理念强调,对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性来实现,而不是将它嵌入到模板中(实际上99%的业务逻辑,你在模板中也写不下)。
<div id="computed-basics"> <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span> </div>
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')
代码还是那句代码,不过我们将它放到computed选项卡里,以一个属性的形式存在。
所谓的计算,就是指这个属性不是直接可以得到的,而是需要通过执行一些计算代码,转换出来的。
我们可以尝试清空 data
中 books
数组的值,你将看到 publishedBooksMessage
属性会相应地更改。
每个计算属性都类似data选项卡中的属性,会自动绑定到应用实例上。
并且,Vue 知道 vm.publishedBookMessage
依赖于 vm.author.books
,因此当 vm.author.books
发生改变时,所有依赖 vm.publishedBookMessage
的绑定也会更新。
也就是说,计算属性不但是响应式的,并且每当它的依赖发生了变化,它本身也会跟着重新计算!
实际上,对于上面的例子,我们可以写一个method实现同样的效果:
<p>{{ calculateBooksMessage() }}</p>
// 在组件中 methods: { calculateBooksMessage() { return this.author.books.length > 0 ? 'Yes' : 'No' } }
我们可以将同样的操作,定义为一个方法,而不是一个计算属性,两种方式的最终结果是完全相同的。
然而,不同的是计算属性是基于它们的响应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。
这就意味着只要 author.books
还没有发生改变,多次访问 publishedBookMessage
计算属性会立即返回之前的计算结果,而不会再次执行函数。
这也意味着下面的计算属性将不再更新,因为 Date.now ()
不是响应式的数据(也就是说不是Vue管理起来的数据,响应能力是Vue提供的):
computed: { now() { return Date.now() } }
相比之下,每当触发重新渲染时,method方法将总会再次执行。
两者的对比总结:
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性
list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于list
。如果没有缓存,我们将不可避免的多次执行list
的 getter!如果你不希望有缓存,请用method
来替代。
getter:将计算属性计算出来的代码
setter:为计算属性赋值的代码
计算属性默认只有 getter,不过在你需要时也可以提供一个 setter,方法如下:
const cc={ data(){ return { firstName:'', lastName:'' } } } // ... computed: { fullName: { // getter get() { return this.firstName + ' ' + this.lastName }, // setter set(newValue) { const names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } // ...
有了setter,我们就可以执行类似 vm.fullName = 'John Doe'
的操作,这时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
计算属性一般是对某个响应式数据进行加工处理获得新数据。比如计算出学生中男生的个数。
而另外一类需求是,监视某个响应式数据,如果它发生变化,就自动调用某个函数。比如,当学生中男生的人数增加时,自动添加对应人数的女生。
Vue通过watch侦听器来实现这类需求。
并且当需要在数据变化时执行异步或开销较大的操作时,watch是最有用的。
例如:
<div id="watch-example"> <p> 这是一个只会回答yes或者no的傻瓜例子: </p> <input v-model="question" /> <p>{{ answer }}</p> </div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 --> <!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 --> <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script> <script> const watchExampleVM = Vue.createApp({ data() { return { question: '', answer: '请以问号结尾;-)' } }, watch: { // whenever question changes, this function will run question(newQuestion, oldQuestion) { if (newQuestion.indexOf('?') > -1) { this.getAnswer() } } }, methods: { getAnswer() { this.answer = '思考中...' axios .get('https://yesno.wtf/api') .then(response => { this.answer = response.data.answer }) .catch(error => { this.answer = '对不起!无法访问远程API. ' + error }) } } }).mount('#watch-example') </script>
测试的时候,要使用英文问号结尾哦!
上面的例子中,使用 watch
选项允许我们执行异步操作 (比如访问一个远程的 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
侦听器有不少有点,但它一定就比计算属性好吗?认真思考一下这个例子:
<div id="demo">{{ fullName }}</div>
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' } }, watch: { firstName(val) { this.fullName = val + ' ' + this.lastName }, lastName(val) { this.fullName = this.firstName + ' ' + val } } }).mount('#demo')
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar' } }, computed: { fullName() { return this.firstName + ' ' + this.lastName } } }).mount('#demo')
还是那句话,没有绝对意义的好坏优劣,只有适用场景的不同。