♠Options-API
认识计算属性computed
什么是计算属性呢?
官方并没有给出直接的概念解释;而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例;
计算属性的用法:
- 选项:computed
- 类型:{ [key: string]: Function | { get: Function, set: Function } }
我们来看三个案例:
- 案例一:我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;
- 案例二:我们有一个分数:score,当score大于60的时候,在界面上显示及格;当score小于60的时候,在界面上显示不及格;
- 案例三:我们有一个变量message,记录一段文字:比如Hello World某些情况下我们是直接显示这段文字;某些情况下我们需要对这段文字进行反转;
我们可以有三种实现思路:
- 思路一:在模板语法中直接使用表达式;
- 思路二:使用method对逻辑进行抽取;
- 思路三:使用计算属性computed;
思路一的实现:模板语法
- 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
- 缺点二:当有多次一样的逻辑时,存在重复的代码;
- 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
思路二的实现:method实现
- 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
- 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
思路三的实现:computed实现
注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加();我们会发现无论是直观上,还是效果上计算属性都是更好的选择;并且计算属性是有缓存的;在数据不发生变化时,计算属性是不需要重新计算的;但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;
查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<button @click="changeFirstName">修改firstName</button>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
firstName: "Kobe",
lastName: "Bryant"
}
},
computed: {
// 计算属性是有缓存的, 当我们多次使用计算属性时, 计算属性中的运算只会执行一次.
// 计算属性会随着依赖的数据(firstName)的改变, 而进行重新计算.
fullName() {
console.log("computed的fullName中的计算");
return this.firstName + " " + this.lastName;
}
},
methods: {
getFullName() {
console.log("methods的getFullName中的计算");
return this.firstName + " " + this.lastName;
},
changeFirstName() {
this.firstName = "Coder"
}
}
}
Vue.createApp(App).mount('#app');
</script>
</body>
</html>
计算属性的setter和getter
但是,如果我们确实想设置计算属性的值呢?这个时候我们也可以给计算属性设置一个setter的方法;
源码如何对setter和getter处理呢?
查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<button @click="changeFullName">修改fullName</button>
<h2>{{fullName}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
firstName: "Kobe",
lastName: "Bryant"
}
},
computed: {
// fullName 的 getter方法
fullName() {
return this.firstName + " " + this.lastName;
},
// fullName的getter和setter方法
fullName: {
get: function() {
return this.firstName + " " + this.lastName;
},
set: function(newValue) {
console.log(newValue);
const names = newValue.split(" ");
this.firstName = names[0];
this.lastName = names[1];
}
}
},
methods: {
changeFullName() {
this.fullName = "XXX Why";
}
}
}
Vue.createApp(App).mount('#app');
</script>
</body>
</html>
认识侦听器watch
什么是侦听器呢?
开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;当数据变化时,template会自动进行更新来显示最新的数据;但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;
侦听器的用法如下:
- 选项:watch
- 类型:{ [key: string]: string | Function | Object | Array}
举个栗子(例子): 比如现在我们希望用户在input中输入一个问题;每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;那么,我们就需要实时的去获取最新的数据变化;
查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
您的问题: <input type="text" v-model="question">
<!-- <button @click="queryAnswer">查找答案</button> -->
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
// 侦听question的变化时, 去进行一些逻辑的处理(JavaScript, 网络请求)
question: "Hello World",
anwser: ""
}
},
watch: {
// question侦听的data中的属性的名称
// newValue变化后的新值
// oldValue变化前的旧值
question: function(newValue, oldValue) {
console.log("新值: ", newValue, "旧值", oldValue);
this.queryAnswer();
}
},
methods: {
queryAnswer() {
console.log(`你的问题${this.question}的答案是哈哈哈哈哈`);
this.anwser = "";
}
}
}
Vue.createApp(App).mount('#app');
</script>
</body>
</html>
我们先来看一个例子:当我们点击按钮的时候会修改info.name的值;这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。
这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:
- 这个时候我们可以使用一个选项deep进行更深层的侦听;
- 注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;
还有另外一个属性,是希望一开始的就会立即执行一次:这个时候我们使用immediate选项;这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;
查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="my-app">
<h2>{{info.name}}</h2>
<h2>{{info.age}}</h2>
<button @click="changeInfo">改变info</button>
<button @click="changeInfoName">改变info.name</button>
<button @click="changeInfoNbaName">改变info.nba.name</button>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
info: { name: "why", age: 18, nba: {name: 'kobe'} }
}
},
watch: {
//默认情况下我们的侦听器只会针对监听的数据本身的改变(内部发生的改变是不能侦听)
// info(newInfo, oldInfo) {
// console.log("newValue:", newInfo, "oldValue:", oldInfo);
// }
// 深度侦听/立即执行(一定会执行一次)
info: {
handler: function(newInfo, oldInfo) {
//新旧的name都是一样的,引用
console.log("newValue:", newInfo.nba.name, "oldValue:", oldInfo.nba.name);
},
deep: true, // 深度侦听
// immediate: true // 立即执行
}
},
methods: {
// changeInfo() {
// this.info = {name: "kobe"};
// },
changeInfoName() {
this.info.name = "kobe";
},
changeInfoNbaName() {
this.info.nba.name = "james";
}
}
}
Vue.createApp(App).mount('#app');
</script>
</body>
</html>
侦听器watch的其他方式(二)
另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性:
还有另外一种方式就是使用 $watch 的API:
我们可以在created的生命周期中,使用 this.$watchs 来侦听;
- 第一个参数是要侦听的源;
- 第二个参数是侦听的回调函数callback;
- 第三个参数是额外的其他选项,比如deep、immediate;
案例
查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="app"></div>
<template id="my-app">
<template v-if="books.length > 0">
<table>
<thead>
<th>序号</th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</thead>
<tbody>
<tr v-for="(book, index) in books">
<td>{{index + 1}}</td>
<td>{{book.name}}</td>
<td>{{book.date}}</td>
<td>{{formatPrice(book.price)}}</td>
<td>
<button :disabled="book.count <= 1" @click="decrement(index)">-</button>
<span class="counter">{{book.count}}</span>
<button @click="increment(index)">+</button>
</td>
<td>
<button @click="removeBook(index)">移除</button>
</td>
</tr>
</tbody>
</table>
<h2>总价格: {{formatPrice(totalPrice)}}</h2>
</template>
<template v-else>
<h2>购物车为空~</h2>
</template>
</template>
<script src="../js/vue.js"></script>
<script src="./index.js"></script>
</body>
</html>
查看代码
Vue.createApp({
template: "#my-app",
data() {
return {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.00,
count: 1
},
]
}
},
computed: {
// vue2: filter/map/reduce
totalPrice() {
let finalPrice = 0;
for (let book of this.books) {
finalPrice += book.count * book.price;
}
return finalPrice;
},
// Vue3不支持过滤器了, 推荐两种做法: 使用计算属性/使用全局的方法
filterBooks() {
return this.books.map(item => {
const newItem = Object.assign({}, item);
newItem.price = "¥" + item.price;
return newItem;
})
}
},
methods: {
increment(index) {
// 通过索引值获取到对象
this.books[index].count++
},
decrement(index) {
this.books[index].count--
},
removeBook(index) {
this.books.splice(index, 1);
},
formatPrice(price) {
return "¥" + price;
}
}
}).mount("#app");
查看代码
table {
border: 1px solid #e9e9e9;
border-collapse: collapse;
border-spacing: 0;
}
th, td {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
th {
background-color: #f7f7f7;
color: #5c6b77;
font-weight: 600;
}
.counter {
margin: 0 5px;
}