Vue3的Options-API
续上文:
Vue3学习之旅–爱上Vue3–Vue3基础语法(一)–以及vscode基本使用和快速生成代码片段
Vue3学习之旅–爱上Vue3–Vue3基础语法(二)–模板语法–条件渲染–列表渲染–为什么Vue的v-for需要绑定key属性
复杂data的处理方式
我们知道,在模板中可以直接通过插值语法显示一些data中的数据。但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;
- 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示
- 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算
- 在模板中放入太多的逻辑会让模板过重和难以维护
- 并且如果多个地方都使用到,那么会有大量重复的代码
我们有没有什么方法可以将逻辑抽离出去呢?
可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中,但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;另外一种方式就是使用计算属性computed;
认识计算属性computed
什么是计算属性呢?
官方并没有给出直接的概念解释;而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;
计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例
计算属性的用法
选项:computed
类型:{ [key: string]: Function | { get: Function, set: Function } }
那接下来我们通过案例来理解一下这个计算属性
案例实现思路
我们来看三个案例:
案例一:我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;
案例二:我们有一个分数:score,当score大于60的时候,在界面上显示及格;当score小于60的时候,在界面上显示不及格。
案例三:我们有一个变量message,记录一段文字:比如Hello World。某些情况下我们是直接显示这段文字;某些情况下我们需要对这段文字进行反转;
我们可以有三种实现思路:
- 思路一:在模板语法中直接使用表达式
- 思路二:使用method对逻辑进行抽取;
- 思路三:使用计算属性computed;
实现思路一:模板语法
思路一的实现:模板语法
<template id="my-app">
<!-- 使用插值语法完成字符串的拼接等案例 -->
<h2>{{firstName + lastName}}</h2>
<h2>{{score >= 60 ? '及格' : '不及格'}}</h2>
<h3>{{message.split('').reverse().join('')}}</h3>
</template>
缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
缺点二:当有多次一样的逻辑时,存在重复的代码;
缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
实现思路二:method实现
思路二的实现:method实现
<div id="app"></div>
<template id="my-app">
<h2>{{getFullName()}}</h2>
<h2>{{getResult()}}</h2>
<h2>{{getReverseMessage()}}</h2>
</template>
<script src="../vue3/vue3.js"></script>
<script>
Vue.createApp({
template: '#my-app',
data() {
return {
firstName: '毛',
lastName: '俊',
score: 80,
message: 'hello Vue3!'
}
},
methods: {
getFullName(){
return this.firstName + this.lastName
},
getResult(){
return this.score>=60? '及格' : '不及格'
},
getReverseMessage(){
return this.message.split('').reverse().join('')
}
},
}).mount('#app')
</script>
缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
缺点二:多次使用方法的时候,没有缓存,也需要多次计算
思路三的实现:computed实现
思路三的实现:computed实现
<div id="app"></div>
<template id="my-app">
<h2>{{fullName}}</h2>
<h2>{{result}}</h2>
<h2>{{reverseMessage}}</h2>
</template>
<script src="../vue3/vue3.js"></script>
<script>
Vue.createApp({
template: '#my-app',
data() {
return {
firstName: '毛',
lastName: '俊',
score: 80,
message: 'hello Vue3!'
}
},
// 计算属性 计算属性在多次使用时,如果数据没有发生改变,是会重复使用上次的数据的
computed: {
// 定义一个计算属性叫做 fullName
// 虽然计算属性看起来和函数很相似,但是本质上计算属性其实就是一个属性
fullName() {
return this.firstName + this.lastName
},
result() {
return this.score >= 60 ? '及格' : '不及格'
},
reverseMessage() {
return this.message.split('').reverse().join('')
}
}
}).mount('#app')
</script>
注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;我们会发现无论是直观上,还是效果上计算属性都是更好的选择;并且计算属性是有缓存的;
计算属性 vs methods
在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属 性有缓存的
接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:
<div id="app"></div>
<template id="my-app">
<button @click="firstName = '毛毛'">修改firstName</button>
<!-- 表面上 计算属性和方法所达到的效果的确是一样的 -->
<!-- 但是当我们多次调用计算属性的时候,发现计算属性只被调用一次,因为默认情况下是有缓存的 -->
<!-- 计算属性会随着数据(也就是data里面我们计算属性用到的那部分数据)的改变,而发生改变 -->
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
</template>
<script src="../vue3/vue3.js"></script>
<script>
Vue.createApp({
template: '#my-app',
data() {
return {
firstName: '毛',
lastName: '俊'
}
},
// 计算属性 计算属性在多次使用时,如果数据没有发生改变,是会重复使用上次的数据的
computed: {
// 定义一个计算属性叫做 fullName
// 虽然计算属性看起来和函数很相似,但是本质上计算属性其实就是一个属性
fullName() {
console.log(1);
return this.firstName + this.lastName
}
},
methods: {
getFullName() {
console.log(2);
return this.firstName + this.lastName
}
},
}).mount('#app')
</script>
计算属性的缓存
这是什么原因呢?
-
这是因为计算属性会基于它们的依赖关系进行缓存;
-
在数据不发生变化时,计算属性是不需要重新计算的;
-
但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;
计算属性的setter和getter
计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数(语法糖)。
但是,如果我们确实想设置计算属性的值呢?
这个时候我们也可以给计算属性设置一个setter的方法;
computed: {
// 直接书写一个计算属性,实际上是简写,调用的是计算属性的getter方法
// fullName() {
// return this.firstName + this.lastName
// }
// 计算属性的完整写法
fullName:{
get:function(){
return this.firstName + this.lastName
},
set:function(newVal){
console.log(newVal);
this.firstName = newVal.split('')[0];
this.lastName = newVal.split('')[1];
}
}
}
源码如何对setter和getter处理呢?
你可能觉得很奇怪,Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处 理的呢?
事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已;
认识侦听器watch
什么是侦听器呢?
开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;
当数据变化时,template会自动进行更新来显示最新的数据;
但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;
侦听器的用法如下:
选项:watch
类型:{ [key: string]: string | Function | Object | Array}
侦听器案例
举个栗子(例子):
比如现在我们希望用户在input中输入一个问题;
每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;
那么,我们就需要实时的去获取最新的数据变化;
<div id="app"></div>
<template id="my-app">
<label for="question">
请输入问题:
<input id="question" type="text" v-model="question">
</label>
</template>
<script src="../vue3/vue3.js"></script>
<script>
Vue.createApp({
template: '#my-app',
data() {
return {
question: 'hello Vue3!'
}
},
// 侦听器 侦听某个属性(数据)的变化,并进行一些逻辑的处理
// 在实际工作时:我们侦听器一般不会监听计算属性,一般来说都是侦听元数据(data)
watch: {
// 第一个参数的变化后的新值
// 第二个参数是变化前的旧值
question(newVal, oldVal) {
console.log(newVal,oldVal);
this.getAnswer(newVal);
}
},
methods: {
getAnswer(question) {
console.log(`${question}问题答案是:哈哈哈哈哈`)
}
},
}).mount('#app')
</script>
侦听器watch的配置选项
我们先来看一个例子:
当我们点击按钮的时候会修改info.name的值;
这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。
这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:
这个时候我们可以使用一个选项deep进行更深层的侦听;
注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;
还有另外一个属性,是希望一开始的就会立即执行一次:
这个时候我们使用immediate选项;
这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;
<div id="app"></div>
<template id="my-app">
<button @click="info.gender = '男'">增加info的gender属性</button>
<button @click="info.name = '哈哈哈'">改变info的name属性</button>
<h2 v-for="item of info">{{item}}</h2>
</template>
<script src="../vue3/vue3.js"></script>
<script>
Vue.createApp({
template: '#my-app',
data() {
return {
info: {
name: '毛毛',
age: 22
}
}
},
watch: {
// 直接监听这个info对象,发现属性发生改变了也不会触发
// 特点: 默认情况下只能监听整个对象是否发生改变(也就是是不是换成一个新对象了)
// 1. 如果直接将对象info直接重新赋值为一个新的对象时,可以直接侦听到
// 2. 无法直接侦听到对象内部属性的改变
// info(newVal, oldVal) {
// console.log(newVal, oldVal);
// }
// 深度侦听:
// info: {
// // 开启深度侦听,可以侦听到对象内部属性的变化了
// deep: true, // 即使对象的属性还是一个对象,仍然还是可以侦听到
// handler(newVal, oldVal) {
// console.log(newVal, oldVal)
// },
// // 侦听器的立即执行
// immediate: true // 在页面刚加载的时候,数据很明显是没有发生变化的(旧的值相当于不存在,也就是undefined),但是我们仍然想执行一遍侦听器的代码逻辑,就可以让 immediate属性为true
// }
// 字符串方法名 侦听器的方法也可以放在methods里面,然后通过字符串的方式赋值,
// info: "changeInfo" // 实际上用的不多
// 也可以使用回调数组 会被逐一调用
info: [
"changeInfo", // 字符串方法名
function handler2(newVal, oldVal) {
console.log(newVal, oldVal, 'handle2');
},
{
handler: function handle3(newVal, oldVal) {
console.log(newVal, oldVal, 'handle3');
}
}
]
},
methods: {
changeInfo() {
}
},
}).mount('#app')
</script>
侦听器watch的其他方式
-
字符串方法名
info: "changeInfo" // 实际上用的不多
-
回调数组的方式
// 也可以使用回调数组 会被逐一调用 info: [ "changeInfo", // 字符串方法名 function handler2(newVal, oldVal) { console.log(newVal, oldVal, 'handle2'); }, { handler: function handle3(newVal, oldVal) { console.log(newVal, oldVal, 'handle3'); } } ]
-
另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性:
watch: { // 直接侦听对象内的某个属性 // 发现只有对象的这个顺序发生改变时,才会被侦听到 // 当然,如果整个对象都直接被替换了,也一样是会被侦听的 "info.name": function (newVal, oldVal) { console.log(newVal, oldVal); } }
-
还有另外一种方式就是使用 $watch 的API:
我们可以在created的生命周期(后续会讲到)中,使用 this.$watchs 来侦听;
- 第一个参数是要侦听的源
- 第二个参数是侦听的回调函数callback;
- 第三个参数是额外的其他选项,比如deep、immediate;
// 生命周期函数 created 在组件被创建的时候执行(后面会讲) created() { // 使用当前组件的$watch方法来创建侦听器 // 参数一:侦听的对象 // 参数二:回调函数 // 参数三:侦听器的配置选项(对象形式) // $watch() 的返回值 unwatch 是一个函数 // 调用这个返回值函数,可以取消侦听器,而直接定义在watch里面的侦听器一般是没办法取消的 const unwatch = this.$watch('info', (newVal, oldVal) => { console.log(newVal, oldVal); }, { deep: true, immediate: true }); // unwatch() // 调用后 侦听器失效 }
-
综合案例
现在我们来做一个相对综合一点的练习:书籍购物车
案例说明:
- 在界面上以表格的形式,显示一些书籍的数据;
- 在底部显示书籍的总价格;
- 点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);
- 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~)
<!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>
<style>
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;
}
</style>
</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="../vue3/vue3.js"></script>
<script>
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() {
const newBook = this.books.map(item => {
// 生成新的对象
const newItem = Object.assign({}, item);
newItem.price = "¥" + item.price;
return newItem;
})
return newBook
}
},
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')
</script>
</body>
</html>