计算属性
计算属性
在模板中使用表达式非常方便,但如果表达式的逻辑过于复杂,模板就会变得臃肿且难以维护,所以引出计算属性。计算属性是以函数形式,在computed中定义。比如可以将模板表达式
<p>
{{ message.split('').reverse().join('')}}
</p>
用计算属性实现:
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>计算后的反转字符串: {{ reversedMessage }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: 'Hello,Java无难事!'
}
},
computed: {
// 计算属性的 getter
reversedMessage(){
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
</script>
</body>
</html>
计算属性默认只有getter,因此不能直接修改计算属性,如果需要,则可以提供一个setter:
<html>
<head>
<meta charset="UTF-8">
<title>计算属性的getter和setter</title>
</head>
<body>
<div id="app">
<p>First name: <input type="text" v-model="firstName"></p>
<p>Last name: <input type="text" v-model="lastName"></p>
<p>{{ fullName }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
firstName: 'Smith',
lastName: "Will"
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
let names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}).mount('#app');
</script>
</body>
</html>
任意修改firstName或lastName的值,fullName也会自动更新,这是调用它的getter函数实现的。在浏览器Console窗口输入vm.fullName='Bruce Willis',可以看到firstName、lastName的值同时发生改变,这是调用fullName的setter函数实现的。
计算属性的缓存
复杂表达式也可以放到方法中实现,然后在绑定表达式中调用方法即可:
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>方法调用后的反转字符串: {{ reversedMessage() }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: 'Hello,Vue.js无难事!'
}
},
methods: {
reversedMessage() {
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
</script>
</body>
</html>
计算属性是基于它的响应式依赖进行缓存的,只有在计算属性的相关响应式依赖发生改变时才会更新值。这就意味着只要message还没有发生变化,多次访问reversedMessage计算属性会立即返回之前计算的结果,而不会在次执行函数;而如果采用方法,不管什么时候访问reversedMessage,该方法都会被调用。
为了对计算属性和方法的区别有更直观的认识,下面同时使用方法和计算属性:
<html>
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="app">
<p>原始字符串: {{ message }}</p>
<p>计算后的反转字符串: {{ reversedMessage }}</p>
<p>方法调用后的反转字符串: {{ reversedMessage2() }}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
message: 'Hello,Vue.js无难事!'
}
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
alert("计算属性");
return this.message.split('').reverse().join('');
}
},
methods: {
reversedMessage2: function () {
alert("方法");
return this.message.split('').reverse().join('');
}
}
}).mount('#app');
let msg = vm.reversedMessage;
msg = vm.reversedMessage2();
</script>
</body>
</html>
在计算属性的getter函数和reversedMessage2方法中调用alert语句显示一个消息框,在根组件实例构建后,分别访问vm.reversedMessage和调用vm.reversedMessage2方法。
使用浏览器打开页面会依次看到计算属性、方法、方法三个消息框,前两个是模板中的花括号被替换显示的,最后一个方法消息框是调用vm.reversedMessage2显示的。可以看到最后对vm.reversedMessage计算属性的访问并没有弹出消息框,这是因为它所依赖的message属性并未发生改变。
下面代码中的计算属性now在初次渲染后不会再更新,因为Date.now()不是响应式依赖
computed:{
now:function(){
return Date.now();
}
}
那么为什么需要缓存呢,假设有一个性能开销比较大的计算属性A,然后可能其他的计算属性依赖于A。如果没有缓存。将不可避免的多次执行A的getter。
v-for和v-if一起使用的替代方案
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>v-for与计算属性</title>
</head>
<body>
<div id="app">
<h1>已完成的工作计划</h1>
<ul>
<li v-for="plan in completedPlans">
{{plan.content}}
</li>
</ul>
<h1>未完成的工作计划</h1>
<ul>
<li v-for="plan in incompletePlans">
{{plan.content}}
</li>
</ul>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const vm = Vue.createApp({
data() {
return {
plans: [
{content: '写《Java无难事》', isComplete: false},
{content: '买菜', isComplete: true},
{content: '写PPT', isComplete: false},
{content: '做饭', isComplete: true},
{content: '打羽毛球', isComplete: false}
]
}
},
computed: {
completedPlans() {
return this.plans.filter(plan => plan.isComplete);
},
incompletePlans(){
return this.plans.filter(plan => !plan.isComplete);
}
}
}).mount('#app');
</script>
</body>
</html>
不建议将v-for和v-if一起使用,这是因为即使由于v-if指令的使用只渲染了部分元素,但在每次重新渲染的时候仍然需要遍历整个列表,而不论渲染的元素内容是否发生了改变。
采用计算属性过滤后再遍历,可以获得以下好处:
- 过滤后的列表只会在plans数组发生变化时才被重新计算,过滤更高效。
- 使用v-for='plan in completedPlans'之后,在渲染的时候只遍历已完成的工作,渲染更高效。
- 解耦渲染层的逻辑,可维护性强。
实例:购物车的实现
要想实现购物车首先得有商品信息:
data() {
return {
books: [
{
id: 1,
title: 'Java无难事',
price: 188,
count: 1
},
{
id: 2,
title: 'VC++深入详解',
price: 168,
count: 1
},
{
id: 3,
title: 'Servlet/JSP深入详解',
price: 139,
count: 1
}
]
}
}
购物车商品的金额是单价和数量相乘,总价是所有商品金额相加,这里采用方法来实现金额,计算属性实现总价,删除操作的事件处理器也定义为一个方法:
methods: {
itemPrice(price, count){
return price * count;
},
deleteItem(index){
this.books.splice(index, 1);
}
},
computed: {
totalPrice(){
let total = 0;
for (let book of this.books) {
total += book.price * book.count;
}
return total;
}
}
接下来使用v-for循环输出商品信息,用表格进行布局:
<html>
<head>
<meta charset="UTF-8">
<title>购物车</title>
<style>
body {
width: 600px;
}
table {
border: 1px solid black;
}
table {
width: 100%;
}
th {
height: 50px;
}
th, td {
border-bottom: 1px solid #ddd;
text-align: center;
}
span {
float: right;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<table>
<tr>
<th>序号</th>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>金额</th>
<th>操作</th>
</tr>
<tr v-for="(book, index) in books" :key="book.id">
<td>{{ book.id }}</td>
<td>{{ book.title }}</td>
<td>{{ book.price }}</td>
<td>
<button v-bind:disabled="book.count === 0"
v-on:click="book.count-=1">-</button>
{{ book.count }}
<button v-on:click="book.count+=1">+</button>
</td>
<td>
{{ itemPrice(book.price, book.count) }}
</td>
<td>
<button @click="deleteItem(index)">删除</button>
</td>
</tr>
</table>
<span>总价:¥{{ totalPrice }}</span>
</div>
说明:
- 在div元素中用v-cloak指令避免页面加载时的闪烁问题,要和css样式[v-cloak]{display:none}一起使用。
- 使用v-for指令时,我们同时使用key属性。
- 商品数量的左右两边各添加了一个减号和加号按钮用于递减和递增商品数量,当商品数量为0时,通过v-bind:disabled='book.count===0'禁用按钮。此外,由于这两个按钮的功能都很简单,所以在使用v-on指令时没有绑定click事件处理方法。
- 单项商品的价格通过itemPrice方法输出,总价通过totalPrice输出,删除通过v-on绑定deleteItem实现。