Vue3开发基础语法(二)
computed
复杂 data 处理方式
在某些情况下,我们可能需要对数据进行一些转换后再显示,或者需要将多个数据结合起来进行显示:
- 比如我们需要对 多个 data 数据进行运算、三元运算符来决定结果、数据进行某种转化 后显示
- 在模板中使用 表达式,可以非常方便的实现,但是设计它们的初衷是用于 简单的运算
- 在模板中放入太多的逻辑会让 模板过重和难以维护
- 并且如果多个地方都使用到,那么会有大量重复的代码
有没有什么方法可以将逻辑抽离出去:
其中一个方法就是将逻辑抽取到一个 method 中,放到 methods 的 options 中
这种做法有一个直观的弊端,就是所有的 data 使用过程都会变成一个 方法的调用
另一个方式就是使用计算属性 computed
计算属性
- 对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性
- 计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例
案例
案例一:我们有两个变量:firstName 和 lastName,希望它们拼接之后在界面上显示
案例二:我们有一个分数:score
- 当score大于60的时候,在界面上显示及格
- 当score小于60的时候,在界面上显示不及格
案例三:我们有一个变量message,记录一段文字:比如Hello World
- 某些情况下我们是直接显示这段文字
- 某些情况下我们需要对这段文字进行反转
实现思路一:模板语法
- 模板中存在大量的复杂逻辑,不便于维护
- 当有多次一样的逻辑时,存在重复的代码
- 多次使用的时候,很多运算也需要多次执行,没有缓存
html
<template id="my-app">
<h2>{{ firstName + ' ' + lastName }}</h2>
<h2>{{ score >= 60 ? '及格' : '不及格' }}</h2>
<h2>{{ message.split("").reverse().join('') }}</h2>
</template>
实现思路二:method 实现
- 我们事实上先显示的是一个结果,但是都变成了一种方法的调用
- 多次使用方法的时候,没有缓存,也需要多次计算
html
<template id="my-app">
<h2>{{ getFullName() }}</h2>
<h2>{{ getResult() }}</h2>
<h2>{{ getReverseMessage() }}</h2>
</template>
<script>
const App = {
methods: {
getFullName() {
return this.firstName + " " + this.lastName
},
getResult() {
return this.score >= 60 ? "及格" : "不及格"
},
getReverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
}
</script>
实现思路三:computed 实现
- 注意:计算属性看起来是一个函数,但是我们在使用的时候不需要加
()
- 无论是直观上,还是效果上计算属性都是更好的选择,计算属性是有缓存的
html
<template id="my-app">
<h2>{{ fullName }}</h2>
<h2>{{ result }}</h2>
<h2>{{ reverseMessage }}</h2>
</template>
<script>
const App = {
computed: {
fullName() {
return this.firstName + " " + this.lastName
},
result() {
return this.score >= 60 ? "及格" : "不及格"
},
reverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
}
</script>
计算属性和methods
html
<template id="my-app">
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<button @click="changeFirstName">修改firstName</button>
</template>
<script>
const App = {
template: '#my-app',
data() {
return {
firstName: "Kobe",
lastName: "Bryant"
}
},
computed: {
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>
- 因为计算属性会基于它们的 依赖关系进行缓存
- 在 数据不发生变化时,计算属性是 不需要重新计算 的
- 但是如果 依赖的数据发生变化,在使用时,计算属性依然 会重新进行计算
计算属性在大多数情况下,只需要一个 getter 方法即可,所以我们会将计算属性直接写成一个函数
- 如果我们确实像 设置计算属性的值,可以给计算属性设置一个 setter 方法
js
const App = {
computed: {
fullName: {
get: function () {
return this.firstName + " " + this.lastName;
},
set: function (newValue) {
const names = newValue.split(" ");
this.firstName = names[0];
this.lastName = names[1];
}
}
}
}
你可能会觉得很奇怪,Vue 内部是如何对我们传入的是一个函数或是一个包含 getter 和 setter 的对象进行处理的,事实上就是内部做了一个判断而已
watch
- 开发中我们在 data 返回的对象中定义了数据,这个数据通过 插值语法等方式绑定到 tmeplate 中
- 当数据变化时,template 会自动进行更新来显示最新的数据
- 但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器 watch 来完成了
html
<template id="my-app">
您的问题: <input type="text" v-model="question">
</template>
<script>
const App = {
data() {
return {
// 侦听question的变化时, 去进行一些逻辑的处理(JavaScript, 网络请求)
question: "Hello World"
}
},
watch: {
question: function(newValue, oldValue) {
console.log("新值: ", newValue, "旧值", oldValue)
}
}
}
</script>
配置选项
当我们点击按钮的时候修改 info.name
的值,这个是不可以侦听到的,watch 只是在侦听 info 的引用变化,对于内部属性的变化是不会做出响应的
- 这时我们可以使用一个 选项deep 进行更深层的侦听
- 如果 希望一开始的就会立即执行一次,可以使用 immediate选项
html
<template id="my-app">
<button @click="changeInfo">改变info</button>
<button @click="changeInfoName">改变info.name</button>
<button @click="changeInfoNbaName">改变info.nba.name</button>
</template>
<script>
const App = {
template: '#my-app',
data() {
return {
info: { name: 'why', age: 18, nba: { name: 'kobe' } }
}
},
watch: {
// 默认情况下侦听器只会针对监听的数据本身的改变(内部发生的改变时不能侦听)
// info(newValue, oldValue) {
// console.log(newValue, oldValue)
// }
info: {
handler: function (newValue, oldValue) {
console.log(newValue, oldValue)
},
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>
其他方式
Vue3 文档没有提到,但是 Vue2 文档中有提到(侦听对象的属性)
js
const App = {
data() {
return {
info: { name: "why" },
friends: [
{ name: "why" },
{ name: "kobe" }
]
}
},
watch: {
"info.name": function (newName, oldName) {
console.log(newName, oldName);
},
"friends[0].name": function (newName, oldName) {
console.log(newName, oldName);
}
}
}
还有一种方式就是使用 $watch
API
- 第一个参数是要侦听的源
- 第二个参数是侦听的回调函数 callback
- 第三个参数是额外的其他选项,比如 deep、immediate
js
const App = {
created() {
const unwatch = this.$watch("info", function (newInfo, oldInfo) {
console.log(newInfo, oldInfo);
}, {
deep: true,
immediate: true
})
// unwatch() // 取消侦听器
}
}
购物车案例
说明:
- 在界面上以表格的形式,显示一些书籍的数据
- 在底部显示书籍的总价格
- 点击 + 或者 - 可以增加或减少书籍数量(如果为 1,那么不能继续-)
- 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~)
HTML
html
<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>
JS
Vue3 不支持过滤器了, 推荐两种做法: 使用计算属性/使用全局的方法
js
Vue.createApp({
template: '#my-app',
data() {
return {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.0,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.0,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.0,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.0,
count: 1
}
]
}
},
computed: {
// vue2: filter/map/reduce
totalPrice() {
let finalPrice = 0
for (let book of this.books) {
finalPrice += book.count * book.price
}
return finalPrice
},
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')