零、文章目录
Vue2基础三、计算属性&侦听器
1、计算属性computed
(1)基础语法
概念:
- 基于现有的数据,计算出来的新属性。 依赖的数据变化,自动重新计算。
- 计算属性会对计算出来的结果缓存,再次使用直接读取缓存,只有依赖项变化了才会自动重新计算,并再次缓存
语法:
- 声明在 computed 配置项中,一个计算属性对应一个函数
- 使用起来和普通属性一样使用 {{ 计算属性名}}
注意:
- computed配置项和data配置项和methods配置项是同级的
- computed中的计算属性不能和data中的属性同名,使用方式和data中的属性一样
- computed中计算属性内部的this指向的是Vue实例
案例:
<!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 #000;
text-align: center;
width: 240px;
}
th,
td {
border: 1px solid #000;
}
h3 {
position: relative;
}
</style>
</head>
<body>
<div id="app">
<h3>小黑的礼物清单</h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }}个</td>
</tr>
</table>
<!-- 目标:统计求和,求得礼物总数 -->
<p>礼物总数:{{ totalCount }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 现有的数据
list: [{
id: 1,
name: '篮球',
num: 1
}, {
id: 2,
name: '玩具',
num: 2
}, {
id: 3,
name: '铅笔',
num: 5
}, ]
},
computed: {
totalCount() {
// 基于现有的数据,编写求值逻辑
// 计算属性函数内部,可以直接通过 this 访问到 app 实例
// console.log(this.list)
// 需求:对 this.list 数组里面的 num 进行求和 → reduce
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
}
})
</script>
</body>
</html>
(2)计算属性 vs 方法
computed计算属性:
-
作用:封装了一段对于数据的处理,求得一个结果
-
语法:
- 写在
computed
配置项中 - 作为属性,直接使用
- js中使用计算属性:
this.计算属性
- 模板中使用计算属性:
{{计算属性}}
- js中使用计算属性:
- 写在
methods方法:
-
作用:给Vue实例提供一个
方法
,调用以处理业务逻辑
。 -
语法:
- 写在
methods
配置项中 - 作为方法调用
- js中调用:
this.方法名()
- 模板中调用
{{方法名()}}
或者@事件名=“方法名”
- js中调用:
- 写在
计算属性的优势:
- 计算属性会对计算出来的结果缓存,再次使用直接读取缓存,只有依赖项变化了才会自动重新计算,并再次缓存
<!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 #000;
text-align: center;
width: 300px;
}
th,
td {
border: 1px solid #000;
}
h3 {
position: relative;
}
span {
position: absolute;
left: 145px;
top: -4px;
width: 16px;
height: 16px;
color: white;
font-size: 12px;
text-align: center;
border-radius: 50%;
background-color: #e63f32;
}
</style>
</head>
<body>
<div id="app">
<h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3>
<h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3>
<h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3>
<h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3>
<table>
<tr>
<th>名字</th>
<th>数量</th>
</tr>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.num }}个</td>
</tr>
</table>
<p>礼物总数:{{ totalCountFn() }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 现有的数据
list: [{
id: 1,
name: '篮球',
num: 3
}, {
id: 2,
name: '玩具',
num: 2
}, {
id: 3,
name: '铅笔',
num: 5
}, ]
},
methods: {
totalCountFn() {
console.log('methods方法执行了')
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
},
computed: {
// 计算属性:有缓存的,一旦计算出来结果,就会立刻缓存
// 下一次读取 → 直接读缓存就行 → 性能特别高
// totalCount () {
// console.log('计算属性执行了')
// let total = this.list.reduce((sum, item) => sum + item.num, 0)
// return total
// }
}
})
</script>
</body>
</html>
(3)完整写法
- 计算属性默认的简写,只能读取访问,
不能 "修改"
。 - 如果要
"修改"
→ 需要写计算属性的完整写法
。
<!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>
input {
width: 30px;
}
</style>
</head>
<body>
<div id="app">
姓:<input type="text" v-model="firstName"> + 名:
<input type="text" v-model="lastName"> =
<span>{{ fullName }}</span><br><br>
<button @click="changeName">改名卡</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
firstName: '刘',
lastName: '备',
},
methods: {
changeName() {
this.fullName = '黄忠'
}
},
computed: {
// 简写 → 获取,没有配置设置的逻辑
// fullName () {
// return this.firstName + this.lastName
// }
// 完整写法 → 获取 + 设置
fullName: {
// (1) 当fullName计算属性,被获取求值时,执行get(有缓存,优先读缓存)
// 会将返回值作为,求值的结果
get() {
return this.firstName + this.lastName
},
// (2) 当fullName计算属性,被修改赋值时,执行set
// 修改的值,传递给set方法的形参
set(value) {
// console.log(value.slice(0, 1))
// console.log(value.slice(1))
this.firstName = value.slice(0, 1)
this.lastName = value.slice(1)
}
}
}
})
</script>
</body>
</html>
(4)成绩案例
<!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" />
<link rel="stylesheet" href="./styles/index.css" />
<title>Document</title>
</head>
<body>
<div id="app" class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="list.length > 0">
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.subject }}</td>
<!-- 需求:不及格的标红, < 60 分, 加上 red 类 -->
<td :class="{ red: item.score < 60 }">{{ item.score }}</td>
<td><a @click.prevent="del(item.id)" href="http://www.baidu.com">删除</a></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总分:{{ totalScore }}</span>
<span style="margin-left: 50px">平均分:{{ averageScore }}</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" v-model.trim="subject" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" v-model.number="score" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button @click="add" class="submit">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
list: [{
id: 1,
subject: '语文',
score: 62
}, {
id: 7,
subject: '数学',
score: 89
}, {
id: 12,
subject: '英语',
score: 70
}, ],
subject: '',
score: ''
},
computed: {
totalScore() {
return this.list.reduce((sum, item) => sum + item.score, 0)
},
averageScore() {
if (this.list.length === 0) {
return 0
}
return (this.totalScore / this.list.length).toFixed(2)
}
},
methods: {
del(id) {
// console.log(id)
this.list = this.list.filter(item => item.id !== id)
},
add() {
if (!this.subject) {
alert('请输入科目')
return
}
if (typeof this.score !== 'number') {
alert('请输入正确的成绩')
return
}
this.list.unshift({
id: +new Date(),
subject: this.subject,
score: this.score
})
this.subject = ''
this.score = ''
}
}
})
</script>
</body>
</html>
2、侦听器watch
(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>
</head>
<body>
<div id="app">
<textarea v-model="words"></textarea>
<textarea v-model="obj.words"></textarea>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
words: '',
obj: {
words: ''
}
},
watch: {
// 该方法会在数据变化时调用执行
// newValue新值, oldValue老值(一般不用)
words(newValue) {
console.log('变化了', newValue)
},
'obj.words' (newValue) {
console.log('变化了', newValue)
}
}
})
</script>
</body>
</html>
(2)完整写法
- 完整写法:添加额外
配置项
deep: true
对复杂类型深度监视immediate: true
初始化立刻执行一次handler方法
<!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">
<textarea v-model="obj.words"></textarea>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
obj: {
words: '小黑'
}
},
watch: {
obj: {
deep: true, // 深度监视
immediate: true, // 立刻执行,一进入页面handler就立刻执行一次
handler(newValue) {
console.log('变化了', newValue)
}
}
}
})
</script>
</body>
</html>
3、综合案例-水果购物车
(1)功能需求
(2)总体思路
-
渲染功能:
v-if/v-else v-for :class
-
删除功能:
点击传参
filter
过滤覆盖原数组 -
修改个数:
点击传参
find
找对象 -
全选反选:计算属性
computed
完整写法get/set
-
统计选中的总价和总数量: 计算属性
computed
reduce
条件求和 -
持久化到本地:
watch
监视,localStorage
,JSON.stringify
,JSON.parse
(3)代码实现
<!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" />
<link rel="stylesheet" href="./css/inputnumber.css" />
<link rel="stylesheet" href="./css/index.css" />
<title>购物车</title>
</head>
<body>
<div class="app-container" id="app">
<!-- 顶部banner -->
<div class="banner-box"><img src="./img/fruit.jpg" alt="" /></div>
<!-- 面包屑 -->
<div class="breadcrumb">
<span>🏠</span> /
<span>购物车</span>
</div>
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0">
<div class="table">
<!-- 头部 -->
<div class="thead">
<div class="tr">
<div class="th">选中</div>
<div class="th th-pic">图片</div>
<div class="th">单价</div>
<div class="th num-th">个数</div>
<div class="th">小计</div>
<div class="th">操作</div>
</div>
</div>
<!-- 身体 -->
<div class="tbody">
<div v-for="(item, index) in fruitList" :key="item.id" class="tr" :class="{ active: item.isChecked }">
<div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
<div class="td"><img :src="item.icon" alt="" /></div>
<div class="td">{{ item.price }}</div>
<div class="td">
<div class="my-input-number">
<button :disabled="item.num <= 1" class="decrease" @click="sub(item.id)"> - </button>
<span class="my-input__inner">{{ item.num }}</span>
<button class="increase" @click="add(item.id)"> + </button>
</div>
</div>
<div class="td">{{ item.num * item.price }}</div>
<div class="td"><button @click="del(item.id)">删除</button></div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 全选 -->
<label class="check-all">
<input type="checkbox" v-model="isAll"/>
全选
</label>
<div class="right-box">
<!-- 所有商品总价 -->
<span class="price-box">总价 : ¥ <span class="price">{{ totalPrice }}</span></span>
<!-- 结算按钮 -->
<button class="pay">结算( {{ totalCount }} )</button>
</div>
</div>
</div>
<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const defaultArr = [{
id: 1,
icon: './img/火龙果.png',
isChecked: true,
num: 2,
price: 6,
}, {
id: 2,
icon: './img/荔枝.png',
isChecked: false,
num: 7,
price: 20,
}, {
id: 3,
icon: './img/榴莲.png',
isChecked: false,
num: 3,
price: 40,
}, {
id: 4,
icon: './img/鸭梨.png',
isChecked: true,
num: 10,
price: 3,
}, {
id: 5,
icon: './img/樱桃.png',
isChecked: false,
num: 20,
price: 34,
}, ]
const app = new Vue({
el: '#app',
data: {
// 水果列表
fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr,
},
computed: {
// 默认计算属性:只能获取不能设置,要设置需要写完整写法
// isAll () {
// // 必须所有的小选框都选中,全选按钮才选中 → every
// return this.fruitList.every(item => item.isChecked)
// }
// 完整写法 = get + set
isAll: {
get() {
return this.fruitList.every(item => item.isChecked)
},
set(value) {
// 基于拿到的布尔值,要让所有的小选框 同步状态
this.fruitList.forEach(item => item.isChecked = value)
}
},
// 统计选中的总数 reduce
totalCount() {
return this.fruitList.reduce((sum, item) => {
if (item.isChecked) {
// 选中 → 需要累加
return sum + item.num
} else {
// 没选中 → 不需要累加
return sum
}
}, 0)
},
// 总计选中的总价 num * price
totalPrice() {
return this.fruitList.reduce((sum, item) => {
if (item.isChecked) {
return sum + item.num * item.price
} else {
return sum
}
}, 0)
}
},
methods: {
del(id) {
this.fruitList = this.fruitList.filter(item => item.id !== id)
},
add(id) {
// 1. 根据 id 找到数组中的对应项 → find
const fruit = this.fruitList.find(item => item.id === id)
// 2. 操作 num 数量
fruit.num++
},
sub(id) {
// 1. 根据 id 找到数组中的对应项 → find
const fruit = this.fruitList.find(item => item.id === id)
// 2. 操作 num 数量
fruit.num--
}
},
watch: {
fruitList: {
deep: true,
handler(newValue) {
// 需要将变化后的 newValue 存入本地 (转JSON)
localStorage.setItem('list', JSON.stringify(newValue))
}
}
}
})
</script>
</body>
</html>
index.css
.app-container {
padding-bottom: 300px;
width: 800px;
margin: 0 auto;
}
@media screen and (max-width: 800px) {
.app-container {
width: 600px;
}
}
.app-container .banner-box {
border-radius: 20px;
overflow: hidden;
margin-bottom: 10px;
}
.app-container .banner-box img {
width: 100%;
}
.app-container .nav-box {
background: #ddedec;
height: 60px;
border-radius: 10px;
padding-left: 20px;
display: flex;
align-items: center;
}
.app-container .nav-box .my-nav {
display: inline-block;
background: #5fca71;
border-radius: 5px;
width: 90px;
height: 35px;
color: white;
text-align: center;
line-height: 35px;
margin-right: 10px;
}
.breadcrumb {
font-size: 16px;
color: gray;
}
.table {
width: 100%;
text-align: left;
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
}
.th {
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
text-align: left;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
transition: background 0.3s ease;
}
.th.num-th {
flex: 1.5;
}
.th {
text-align: center;
}
.th:nth-child(4),
.th:nth-child(5),
.th:nth-child(6),
.th:nth-child(7) {
text-align: center;
}
.th.th-pic {
flex: 1.3;
}
.th:nth-child(6) {
flex: 1.3;
}
.th,
.td {
position: relative;
padding: 16px 16px;
overflow-wrap: break-word;
flex: 1;
}
.pick-td {
font-size: 14px;
}
.main,
.empty {
border: 1px solid #f0f0f0;
margin-top: 10px;
}
.tr {
display: flex;
cursor: pointer;
border-bottom: 1px solid #ebeef5;
}
.tr.active {
background-color: #f5f7fa;
}
.td {
display: flex;
justify-content: center;
align-items: center;
}
.table img {
width: 100px;
height: 100px;
}
button {
outline: 0;
box-shadow: none;
color: #fff;
background: #d9363e;
border-color: #d9363e;
color: #fff;
background: #d9363e;
border-color: #d9363e;
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px 0 rgb(0 0 0 / 2%);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
touch-action: manipulation;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
}
button.pay {
background-color: #3f85ed;
margin-left: 20px;
}
.bottom {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 20px;
border: 1px solid #f0f0f0;
border-top: none;
padding-left: 20px;
}
.right-box {
display: flex;
align-items: center;
}
.check-all {
cursor: pointer;
}
.price {
color: hotpink;
font-size: 30px;
font-weight: 700;
}
.price-box {
display: flex;
align-items: center;
}
.empty {
padding: 20px;
text-align: center;
font-size: 30px;
color: #909399;
}
.my-input-number {
display: flex;
}
.my-input-number button {
height: 40px;
color: #333;
border: 1px solid #dcdfe6;
background-color: #f5f7fa;
}
.my-input-number button:disabled {
cursor: not-allowed!important;
}
.my-input-number .my-input__inner {
height: 40px;
width: 50px;
padding: 0;
border: none;
border-top: 1px solid #dcdfe6;
border-bottom: 1px solid #dcdfe6;
}
inputnumber.css
.my-input-number {
position: relative;
display: inline-block;
width: 140px;
line-height: 38px;
}
.my-input-number span {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.my-input-number .my-input {
display: block;
position: relative;
font-size: 14px;
width: 100%;
}
.my-input-number .my-input__inner {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
padding-left: 50px;
padding-right: 50px;
text-align: center;
}
.my-input-number .my-input-number__decrease,
.my-input-number .my-input-number__increase {
position: absolute;
z-index: 1;
top: 1px;
width: 40px;
height: auto;
text-align: center;
background: #f5f7fa;
color: #606266;
cursor: pointer;
font-size: 13px;
}
.my-input-number .my-input-number__decrease {
left: 1px;
border-radius: 4px 0 0 4px;
border-right: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__increase {
right: 1px;
border-radius: 0 4px 4px 0;
border-left: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__decrease.is-disabled,
.my-input-number .my-input-number__increase.is-disabled {
color: #c0c4cc;
cursor: not-allowed;
}