Vue2:基础入门2
Date: July 29, 2023
Sum: Computed计算属性、watch侦听器、水果车
计算属性
基础及案例:
概念:
基于现有的数据,计算出来的新属性。 依赖的数据变化,自动重新计算。
计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值,供组件渲染 DOM 时使用。
应用场景:
表单中的数据变化,会导致结果也跟着变化
语法:先声明后使用
1-声明在 computed 配置项中,一个计算属性对应一个函数
2-使用起来和普通属性一样使用 {{ 计算属性名}}
案例: 小黑的礼物清单
需求:礼物总数与表单中的数量之和同步
-
Code:
<!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>礼物总数:{{ plus }} 个</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: { plus() { let total = this.list.reduce((sum, item) => sum += item.num, 0) return total } } }) </script> </body> </html>
reduce参考:https://blog.csdn.net/qq_38970408/article/details/121018660
注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有 return 返回值!
使用注意点:
① 计算属性必须定义在 computed 节点中
② 计算属性必须是一个 function 函数
③ 计算属性必须有返回值
④ 计算属性必须当做普通属性使用
比如 {{plus}}, 而非是这样用:{{plus()}}
计算属性 vs 方法
1-computed计算属性
作用:封装了一段对于数据的处理,求得一个结果
语法:
- 写在computed配置项中
- 作为属性,直接使用js中使用计算属性: this.计算属性模板中使用计算属性:{{计算属性}}
2-methods计算属性
作用:给Vue实例提供一个方法,调用以处理业务逻辑。
语法:
- 写在methods配置项中
- 作为方法调用
- js中调用:this.方法名()
- 模板中调用 {{方法名()}} 或者 @事件名=“方法名”
计算属性的优势
1-缓存特性:(提升性能)计算属性会对计算出来的结果缓存,再次使用直接读取缓存,依赖项变化了,会自动重新计算 → 并再次缓存
2-methods没有缓存特性
案例:
理解:计算属性会对计算的结果缓存,下一次直接提缓存结果即可,如图计算属性就仅计算了一次,而methods是调用几次就计算几次。很明显,前者资源开销更低。
-
Code:
<template> <div> <!-- 通过v-model进行双向数据绑定。 .number指自动将用户的输入值转为数值类型 --> <input type="text" v-model.number="count" /> <p>{{ count }} 乘以2的值为: {{ plus }}</p> <p>{{ count }} 乘以2的值为: {{ plus }}</p> <p>{{ count }} 乘以2的值为: {{ plus }}</p> <hr/> <p>{{ count }} 乘以2的值为: {{ m_plus() }}</p> <p>{{ count }} 乘以2的值为: {{ m_plus() }}</p> <p>{{ count }} 乘以2的值为: {{ m_plus() }}</p> </div> </template> <script> export default { name: 'MyCounter', data() { return { count: 1, } }, computed: { // 计算属性:监听data中count值的变化,自动计算出count*2之后的新值 plus() { console.log("计算属性被执行了"); return this.count * 2 } }, methods: { m_plus() { console.log("方法被执行了"); return this.count * 2 } } } </script>
注意:
计算属性的结果会被缓存,性能好
方法的计算结果无法被缓存,性能低
计算属性完整写法:
计算属性也是属性,能访问,也应能修改
- 计算属性默认的简写,只能读取访问,不能 “修改”
- 如果要 “修改” → 需要写计算属性的完整写法
注:建议结合以下例子理解
computed: {
计算属性名: {
get(): {
代码逻辑
return res
},
set(修改的值): {
代码逻辑
}
}
}
案例:通过改名卡,能够修改姓和名
-
Code:
<!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>
计算属性案例:
案例需求,使用计算属性动态计算:
① 已勾选的商品总个数 ② 已勾选的商品总价 ③ 结算按钮的禁用状态
-
Code:
computed: { //动态计算出勾选水果的总数量 total() { let t = 0 this.fruitlist.forEach(x => { if(x.state) { t += x.count } }) return t }, amount() { let a = 0 this.fruitlist .filter(x => x.state) .forEach(x => { a += x.price * x.count }) return a }, isDisabled() { this.total === 0 } },
效果:
注意:
这里的this指的是当前组件实例
computed: {
isDisabled() {
this.toal === 0
}
}
个人总结:
计算属性
概念:计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值,供组件渲染 DOM 时使用。
export default {
name: 'MyCounter',
data() {
return {
count: 1,
}
},
computed: {
// 计算属性:监听data中count值的变化,自动计算出count*2之后的新值
plus() {
return this.count * 2
}
}
}
watch 侦听器
概念及使用:
概念:
监视数据变化,执行一些业务逻辑或异步操作。
应用场景:
1-监视用户名的变化并发起请求,判断用户名是否可用。
2-实时翻译
语法:
在 watch 节点下,定义自己的侦听器。
export default {
data() {
return {
username: ''
}
},
watch: {
// 监听 username 的值的变化
// 形参列表中,第一个值是“变化后的新值”,第二个值是“变化之前的旧值”
// 注:oldVal 可以不传
username(newVal, oldVal) {
console.log(newVal, oldVal);
}
}
}
进一步:如果你想要监视某个复杂数据类型中的子属性
const app = new Vue({
el: '#app',
data: {
// words: ''
obj: {
words: ''
}
},
watch: {
'obj.words' (newValue) {
console.log('变化了', newValue)
}
}
案例:实时翻译
-
Code:
<!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> * { margin: 0; padding: 0; box-sizing: border-box; font-size: 18px; } #app { padding: 10px 20px; } .query { margin: 10px 0; } .box { display: flex; } textarea { width: 300px; height: 160px; font-size: 18px; border: 1px solid #dedede; outline: none; resize: none; padding: 10px; } textarea:hover { border: 1px solid #1589f5; } .transbox { width: 300px; height: 160px; background-color: #f0f0f0; padding: 10px; border: none; } .tip-box { width: 300px; height: 25px; line-height: 25px; display: flex; } .tip-box span { flex: 1; text-align: center; } .query span { font-size: 18px; } .input-wrap { position: relative; } .input-wrap span { position: absolute; right: 15px; bottom: 15px; font-size: 12px; } .input-wrap i { font-size: 20px; font-style: normal; } </style> </head> <body> <div id="app"> <!-- 条件选择框 --> <div class="query"> <span>翻译成的语言:</span> <select> <option value="italy">意大利</option> <option value="english">英语</option> <option value="german">德语</option> </select> </div> <!-- 翻译框 --> <div class="box"> <div class="input-wrap"> <textarea v-model="obj.words"></textarea> <span><i>⌨️</i>文档翻译</span> </div> <div class="output-wrap"> <div class="transbox">mela</div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script> // 接口地址:https://applet-base-api-t.itheima.net/api/translate // 请求方式:get // 请求参数: // (1)words:需要被翻译的文本(必传) // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 // ----------------------------------------------- const app = new Vue({ el: '#app', data: { // words: '' obj: { words: '' } }, // 具体讲解:(1) watch语法 (2) 具体业务实现 watch: { // 该方法会在数据变化时调用执行 // newValue新值, oldValue老值(一般不用) // words (newValue) { // console.log('变化了', newValue) // } 'obj.words' (newValue) { console.log('变化了', newValue) } } }) </script> </body> </html>
补充案例:使用 watch 检测用户名是否可用
-
特点:采用解构获取数据
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
复习并理解:
返回promise对象:
const res = axios.get('http://www.escook.cn/api/finduser/' + newVal)
若返回promise对象,我们可以通过await/async进行简化,返回的则是一个数据对象,
async username(newVal, oldVal) { console.log(newVal, oldVal); const res = await axios.get('http://www.escook.cn/api/finduser/' + newVal) console.log(res); }
防抖优化处理:
案例:实时翻译
需求:防止一直输入,一直及时翻译,而是等输入完,再翻译,以提高性能
-
Code:
<!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> * { margin: 0; padding: 0; box-sizing: border-box; font-size: 18px; } #app { padding: 10px 20px; } .query { margin: 10px 0; } .box { display: flex; } textarea { width: 300px; height: 160px; font-size: 18px; border: 1px solid #dedede; outline: none; resize: none; padding: 10px; } textarea:hover { border: 1px solid #1589f5; } .transbox { width: 300px; height: 160px; background-color: #f0f0f0; padding: 10px; border: none; } .tip-box { width: 300px; height: 25px; line-height: 25px; display: flex; } .tip-box span { flex: 1; text-align: center; } .query span { font-size: 18px; } .input-wrap { position: relative; } .input-wrap span { position: absolute; right: 15px; bottom: 15px; font-size: 12px; } .input-wrap i { font-size: 20px; font-style: normal; } </style> </head> <body> <div id="app"> <!-- 条件选择框 --> <div class="query"> <span>翻译成的语言:</span> <select> <option value="italy">意大利</option> <option value="english">英语</option> <option value="german">德语</option> </select> </div> <!-- 翻译框 --> <div class="box"> <div class="input-wrap"> <textarea v-model="obj.words"></textarea> <span><i>⌨️</i>文档翻译</span> </div> <div class="output-wrap"> <div class="transbox">{{ result }}</div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script> // 接口地址:https://applet-base-api-t.itheima.net/api/translate // 请求方式:get // 请求参数: // (1)words:需要被翻译的文本(必传) // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 // ----------------------------------------------- const app = new Vue({ el: '#app', data: { // words: '' obj: { words: '' }, result: '', // 翻译结果 // timer: null // 延时器id }, // 具体讲解:(1) watch语法 (2) 具体业务实现 watch: { // 该方法会在数据变化时调用执行 // newValue新值, oldValue老值(一般不用) // words (newValue) { // console.log('变化了', newValue) // } 'obj.words' (newValue) { // console.log('变化了', newValue) // 防抖: 延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行 clearTimeout(this.timer) // timer存取延时器id this.timer = setTimeout(async () => { const res = await axios({ url: 'https://applet-base-api-t.itheima.net/api/translate', params: { words: newValue } }) this.result = res.data.data console.log(res.data.data) }, 300) } } }) </script> </body> </html>
注意:如果你想使用一些非响应式的数据,你可以通过this把它们挂载到对象身上,而非存到data中
watch侦听器完整写法
如果结果需要受到多个条件来查询控制,我们通常会把它放入到一个obj对象中进行控制。
比如下面:翻译的结果受到 语言 与 被翻译的内容 两方面控制
问题:如果结果会受到多方面的控制,该怎么办?
解决方案:添加额外配置项
(1) deep: true 对复杂类型进行深度件事
(2) immediate: true 初始化立刻执行一次handler方法
采用deep:翻译结果会受到 语言与被翻译内容 二者影响
watch: {
// 该方法会在数据变化时调用执行
// newValue新值, oldValue老值(一般不用)
// words (newValue) {
// console.log('变化了', newValue)
// }
obj: {
deep: true,
async handler(newValue) { // newValue指obj
clearTimeout(this.timer)
// 你可以把上面data中的timer清除,下面依然可以执行
// 因为this会把timer挂载到obj身上
this.timer = setTimeout(async() => {
const res = await axios({
url: 'https://applet-base-api-t.itheima.net/api/translate',
params: newValue
})
this.result = res.data.data
console.log(res.data.data);
}, 200)
}
},
}
注:以上我直接把obj作为对象以参数的方式传入,axios会把obj转成参数(用&拼接对象属性)
采用immediate: 文本框中预留内容,页面一加载就会被自动翻译。原理上讲,就是一进页面就会执行一次下面的 handler 方法
watch: {
obj: {
deep: true, // 深度监视
immediate: true, // 立刻执行,一进入页面handler就立刻执行一次
handler (newValue) {
clearTimeout(this.timer)
this.timer = setTimeout(async () => {
const res = await axios({
url: 'https://applet-base-api-t.itheima.net/api/translate',
params: newValue
})
this.result = res.data.data
console.log(res.data.data)
}, 300)
}
}
}
结合以上二者的案例效果:
-
Code:
<!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> * { margin: 0; padding: 0; box-sizing: border-box; font-size: 18px; } #app { padding: 10px 20px; } .query { margin: 10px 0; } .box { display: flex; } textarea { width: 300px; height: 160px; font-size: 18px; border: 1px solid #dedede; outline: none; resize: none; padding: 10px; } textarea:hover { border: 1px solid #1589f5; } .transbox { width: 300px; height: 160px; background-color: #f0f0f0; padding: 10px; border: none; } .tip-box { width: 300px; height: 25px; line-height: 25px; display: flex; } .tip-box span { flex: 1; text-align: center; } .query span { font-size: 18px; } .input-wrap { position: relative; } .input-wrap span { position: absolute; right: 15px; bottom: 15px; font-size: 12px; } .input-wrap i { font-size: 20px; font-style: normal; } </style> </head> <body> <div id="app"> <!-- 条件选择框 --> <div class="query"> <span>翻译成的语言:</span> <select v-model="obj.lang"> <option value="italy">意大利</option> <option value="english">英语</option> <option value="german">德语</option> </select> </div> <!-- 翻译框 --> <div class="box"> <div class="input-wrap"> <textarea v-model="obj.words"></textarea> <span><i>⌨️</i>文档翻译</span> </div> <div class="output-wrap"> <div class="transbox">{{ result }}</div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script> // 需求:输入内容,修改语言,都实时翻译 // 接口地址:https://applet-base-api-t.itheima.net/api/translate // 请求方式:get // 请求参数: // (1)words:需要被翻译的文本(必传) // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 // ----------------------------------------------- const app = new Vue({ el: '#app', data: { obj: { words: '小黑', lang: 'italy' }, result: '', // 翻译结果 }, watch: { obj: { deep: true, // 深度监视 immediate: true, // 立刻执行,一进入页面handler就立刻执行一次 handler (newValue) { clearTimeout(this.timer) this.timer = setTimeout(async () => { const res = await axios({ url: 'https://applet-base-api-t.itheima.net/api/translate', params: newValue }) this.result = res.data.data console.log(res.data.data) }, 300) } } // 'obj.words' (newValue) { // clearTimeout(this.timer) // this.timer = setTimeout(async () => { // const res = await axios({ // url: 'https://applet-base-api-t.itheima.net/api/translate', // params: { // words: newValue // } // }) // this.result = res.data.data // console.log(res.data.data) // }, 300) // } } }) </script> </body> </html>
- immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项。实例代码如下:
- deep 选项
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项,代码示例如下:
data() {
return {
username: 'admin',
info: {
username: 'zs' //info中包含 username 属性
}
}
},
watch: {
info: { //直接监听 info 对象的变化
async handler(newVal) {
const { data: res } = await axios.get('https://www.escook.cn/api/finduser' + newVal.username)
console.log(res);
},
deep: true // 需要使用 deep 选项, 否则 username 值的变化无法被监听到
}
}
- 监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:
- 计算属性 vs 侦听器
计算属性和侦听器侧重的应用场景不同:
计算属性侧重于监听多个值的变化,最终计算并返回一个新值
计算属性侧重于得到最后的一个结果
侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
侦听器侧重于去执行某个业务逻辑,业务逻辑执行完,侦听器的目的就达到了
个人总结:
watch 侦听器:
概念:watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作
简单写法:
watch: {
数据属性名(newVal, oldVal) {
业务逻辑 或 异步操作
}
‘对象.属性名’(newVal, oldVal) {
业务逻辑 或 异步操作
}
}
完整写法:
watch: {
数据属性名:{
deep: true, // 深度件事
immediate: true, // 是否立刻执行一次 handler
handler (newValue) {
业务逻辑 或 异步操作
}
}
}
注意:
组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项。
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项
综合案例-水果车
需求与思路:
需求说明:
- 渲染功能
- 删除功能
- 修改个数
- 全选反选
- 统计 选中的 总价 和 总数量
- 持久化到本地
实现思路:
1.基本渲染: v-for遍历、:class动态绑定样式
2.删除功能 : v-on 绑定事件,获取当前行的id
3.修改个数 : v-on绑定事件,获取当前行的id,进行筛选出对应的项然后增加或减少
4.全选反选
4-1 必须所有的小选框都选中,全选按钮才选中 → every
4-2 如果全选按钮选中,则所有小选框都选中
4-3 如果全选取消,则所有小选框都取消选中
声明计算属性,判断数组中的每一个checked属性的值,看是否需要全部选
5.统计 选中的 总价 和 总数量 :通过计算属性来计算选中的总价和总数量
6.持久化到本地: 在数据变化时都要更新下本地存储 watch
参考:day2
心得:
实现渲染功能:
对于表单数据,一般采用v-mode处理。
比如处理勾选框
div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
对于处理图片地址,一般用 v-bind ,而不能这样用:
<div class="td"><img src={{item.icon}} alt="" /></div>
以上这样会报错
插值表达式一般用于渲染标签内内容,用法如下:
<p>姓名:{{username}}</p>
-
Code: 已实现渲染功能
<!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="http://autumnfish.cn/static/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 class="tr" :class="{ active: item.isChecked}" v-for="(item , index) in fruitList" :key="item.id"> <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 class="decrease"> - </button> <span class="my-input__inner">{{ item.num }}</span> <button class="increase"> + </button> </div> </div> <div class="td">{{ item.price * item.num }}</div> <div class="td"><button>删除</button></div> </div> </div> </div> <!-- 底部 --> <div class="bottom"> <!-- 全选 --> <label class="check-all"> <input type="checkbox" /> 全选 </label> <div class="right-box"> <!-- 所有商品总价 --> <span class="price-box">总价 : ¥ <span class="price">24</span></span> <!-- 结算按钮 --> <button class="pay">结算( 6 )</button> </div> </div> </div> <!-- 空车 --> <div class="empty">🛒空空如也</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { // 水果列表 fruitList: [ { id: 1, icon: 'http://autumnfish.cn/static/火龙果.png', isChecked: true, num: 2, price: 6, }, { id: 2, icon: 'http://autumnfish.cn/static/荔枝.png', isChecked: false, num: 7, price: 20, }, { id: 3, icon: 'http://autumnfish.cn/static/榴莲.png', isChecked: false, num: 3, price: 40, }, { id: 4, icon: 'http://autumnfish.cn/static/鸭梨.png', isChecked: true, num: 10, price: 3, }, { id: 5, icon: 'http://autumnfish.cn/static/樱桃.png', isChecked: false, num: 20, price: 34, }, ], }, }) </script> </body> </html>
实现删除、修改与全选反选:
正反选问题:
采用computed来实现正选:即如果上面都勾上,那么下面的全选也勾上
问题是反选,如果我在下面勾上反选,会报计算属性的错误,也就是计算属性没设置全的问题
完整的计算属性
computed: {
计算属性名: {
get(): {
代码逻辑
return res
},
set(修改的值): {
代码逻辑
}
}
}
解决方案:
computed: {
isAll: {
get() {
return this.fruitList.every(item => item.isChecked === true)
},
set(value) {
this.fruitList.forEach(item => item.isChecked = value)
}
}
},
-
Code:
<!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="http://autumnfish.cn/static/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 class="tr" :class="{ active: item.isChecked}" v-for="(item , index) in fruitList" :key="item.id"> <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 class="decrease" @click="sub(item.id)" :disabled="item.num <= 0"> - </button> <span class="my-input__inner">{{ item.num }}</span> <button class="increase" @click="add(item.id)"> + </button> </div> </div> <div class="td">{{ item.price * item.num }}</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">24</span></span> <!-- 结算按钮 --> <button class="pay">结算( 6 )</button> </div> </div> </div> <!-- 空车 --> <div class="empty">🛒空空如也</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { // 水果列表 fruitList: [ { id: 1, icon: 'http://autumnfish.cn/static/火龙果.png', isChecked: true, num: 2, price: 6, }, { id: 2, icon: 'http://autumnfish.cn/static/荔枝.png', isChecked: false, num: 7, price: 20, }, { id: 3, icon: 'http://autumnfish.cn/static/榴莲.png', isChecked: false, num: 3, price: 40, }, { id: 4, icon: 'http://autumnfish.cn/static/鸭梨.png', isChecked: true, num: 10, price: 3, }, { id: 5, icon: 'http://autumnfish.cn/static/樱桃.png', isChecked: false, num: 20, price: 34, }, ], }, computed: { isAll: { get() { return this.fruitList.every(item => item.isChecked === true) }, set(value) { this.fruitList.forEach(item => item.isChecked = value) } } }, methods: { del(id) { this.fruitList = this.fruitList.filter(item => item.id !== id) }, add(id) { const fruit = this.fruitList.find(item => item.id === id) fruit.num++ }, sub(id) { const fruit = this.fruitList.find(item => item.id === id) if(fruit.num === 0){ alert('不能再减少了') console.log(12); return } fruit.num-- } } }) </script> </body> </html>
实现总价:
问题:
reduce与箭头的使用方式:如果里面箭头函数{}包含多行代码,记得要return
let total = this.fruitList.reduce((sum, item) => {
if(item.isChecked === true){
return sum += item.num
}else {
return sum
}
}, 0)
-
Code:
持久化到本地:
问题:任何数据的变化,都需要监视并存取
策略:通过watch的deep: true以及 localStorage来进行处理
watch: {
fruitList: {
deep: true,
handler(newValue) {
localStorage.setItem('list', JSON.stringify(newValue))
}
}
}
通过浏览器开发者模式查看信息:
补充:通过这个键清空本地缓存
所有代码:
-
Code:
<!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="http://autumnfish.cn/static/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 class="tr" :class="{ active: item.isChecked}" v-for="(item , index) in fruitList" :key="item.id"> <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 class="decrease" @click="sub(item.id)" :disabled="item.num <= 0"> - </button> <span class="my-input__inner">{{ item.num }}</span> <button class="increase" @click="add(item.id)"> + </button> </div> </div> <div class="td">{{ item.price * item.num }}</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">🛒空空如也</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script> const defaultList = [ { id: 1, icon: 'http://autumnfish.cn/static/火龙果.png', isChecked: true, num: 2, price: 6, }, { id: 2, icon: 'http://autumnfish.cn/static/荔枝.png', isChecked: false, num: 7, price: 20, }, { id: 3, icon: 'http://autumnfish.cn/static/榴莲.png', isChecked: false, num: 3, price: 40, }, { id: 4, icon: 'http://autumnfish.cn/static/鸭梨.png', isChecked: true, num: 10, price: 3, }, { id: 5, icon: 'http://autumnfish.cn/static/樱桃.png', isChecked: false, num: 20, price: 34, }, ] const app = new Vue({ el: '#app', data: { fruitList: JSON.parse(localStorage.getItem('list')) || defaultList, }, computed: { isAll: { get() { return this.fruitList.every(item => item.isChecked === true) }, set(value) { this.fruitList.forEach(item => item.isChecked = value) } }, totalCount() { return this.fruitList.reduce((sum, item) => { if(item.isChecked === true){ return sum += item.num }else { return sum } }, 0) }, totalPrice() { return this.fruitList.reduce((sum, item) => { if(item.isChecked === true) { return sum += (item.price * item.num) }else { return sum } }, 0) } }, methods: { del(id) { this.fruitList = this.fruitList.filter(item => item.id !== id) }, add(id) { const fruit = this.fruitList.find(item => item.id === id) fruit.num++ }, sub(id) { const fruit = this.fruitList.find(item => item.id === id) if(fruit.num === 0){ alert('不能再减少了') console.log(12); return } fruit.num-- }, }, watch: { fruitList: { deep: true, handler(newValue) { localStorage.setItem('list', JSON.stringify(newValue)) } } } }) </script> </body> </html>
业务总结:
- 渲染功能:v-if/v-else v-for :class
- 删除功能:点击传参 filter过滤覆盖原数组
- 修改个数:点击传参find找对象
- 全选反选:计算属性computed 完整写法 get/ set
- 统计选中的总价和总数量:计算属性computed reduce条件求和
- 持久化到本地:watch监视,localStorage, JSON.stringify, JSON.parse