前言
说明:本系列文章会持续更新Vue3的学习过程和遇到的难点。
前提:要求对Vue2有一定基础的了解或者使用,否则会有点费劲哦~
一、computed
computed是计算属性,学习过Vue2应该是有一定了解的,顾名思义是定义一个需要计算得到的一个属性(也就是变量),如果只是需要一个变量可以直接定义,那么计算属性一定是和常规变量有一定区别才需要特殊定义,接下来用一个简单的例子来看看计算属性的作用:
//需求:这里有三个可变num和一个总和total,不论改变哪个num,总和需要实时进行变化。
//先简单写一个HTML,把计算的值展示一下
<template>
<div>
<table width="800px" border>
<thead>
<tr>
<th>num1</th>
<th>num2</th>
<th>num3</th>
<th>总和</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<button type="submit" @click="changeNum1(false)">-</button>
{{ num1 }}
<button type="submit" @click="changeNum1(true)">+</button>
</td>
<td>
<button type="submit" @click="changeNum2(false)">-</button>
{{ num2 }}
<button type="submit" @click="changeNum2(true)">+</button>
</td>
<td>
<button type="submit" @click="changeNum3(false)">-</button>
{{ num3 }}
<button type="submit" @click="changeNum3(true)">+</button>
</td>
<td>
{{ total }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
现在每一个num都支持加减,还有一个总和,接下来用两种不同的方式来直观的感受computed的作用
<script setup lang="ts">
import { ref } from 'vue';
//定义num和total
let total = ref(0)
let num1 = ref(0)
let num2 = ref(0)
let num3 = ref(0)
//改变num1函数
const changeNum1 = (type) => {
if(num1.value > 1 && !type){
num1.value--
}
if(num1.value < 100 && type){
num1.value++
}
getTotal()
}
//改变num2函数
const changeNum2 = (type) => {
if(num2.value > 1 && !type){
num2.value--
}
if(num2.value < 100 && type){
num2.value++
}
getTotal()
}
//改变num3函数
const changeNum3 = (type) => {
if(num3.value > 1 && !type){
num3.value--
}
if(num3.value < 100 && type){
num3.value++
}
getTotal()
}
//计算total总和
const getTotal = () =>{
total.value = num1.value + num2.value + num3.value
}
</script>
这是没有使用computed计算属性的写法,在每次修改num1或者num2或者num3时,都需要调用一次计算total的函数,否则无法达到改变num实时更新total的需求,比较繁琐。来看看使用了computed代码会有怎样的变化:
<script setup lang="ts">
import { ref,computed } from 'vue';
let num1 = ref(0)
let num2 = ref(0)
let num3 = ref(0)
let total = computed(()=>{
return num1.value + num2.value + num3.value
})
const changeNum1 = (type) => {
if(num1.value > 1 && !type){
num1.value--
}
if(num1.value < 100 && type){
num1.value++
}
}
const changeNum2 = (type) => {
if(num2.value > 1 && !type){
num2.value--
}
if(num2.value < 100 && type){
num2.value++
}
}
const changeNum3 = (type) => {
if(num3.value > 1 && !type){
num3.value--
}
if(num3.value < 100 && type){
num3.value++
}
}
</script>
只需要将total使用computed来定义,当组成total的每个变量发生变化的时候,都会重新计算total的值,就和watch监听比较接近,就不需要定义一个计算total的函数,每次改变值的时候再去调用了。
总结:computed的作用就是当一个属性是由多个可变的变量组成时,并且需要实时进行视图更新的变量使用computed来定义。
computed在Vue3中的写法
<script setup lang="ts">
import { computed } from 'vue';
// 1
let total = computed(()=>{
return num1.value + num2.value + num3.value
})
// 2
let total = computed({
set(){},
get(){
return num1.value + num2.value + num3.value
}
})
</script>
- 第一种是computed(参数) 参数直接放个函数return一个返回值
- 第二种是computed(参数) 参数传个对象,里面带set和get方法,在get方法返回这个计算属性
- 其实基本上使用第一种写法就可以了,比较简单一点
1.1、进入实战
了解了computed的作用,那么具体的应用场景有哪些呢?这里来举一个比较经典的案例, 购物网站大家都写过吧,最常见的购物车很熟悉吧,就可以使用到计算属性。接下来看具体实战:
<template>
<div>
<table width="800px" border>
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>总价</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in goodsList" :key="index">
<td>
{{ item.goodsName }}
</td>
<td>
{{ item.price }}
</td>
<td>
<button type="submit" @click="changeNum(item,false)">-</button>
{{ item.num }}
<button type="submit" @click="changeNum(item,true)">+</button>
</td>
<td>
{{ item.price * item.num }}
</td>
<td>
<button @click="deteleBtn(index)">删除</button>
</td>
</tr>
</tbody>
</table>
<p style="text-align:center">
总价格: {{ totalPrice }}
</p>
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed } from 'vue';
//商品数据
const goodsList = reactive([
{goodsName:"铅笔",num:1,price:5},
{goodsName:"黑笔",num:1,price:6},
{goodsName:"红笔",num:1,price:7},
{goodsName:"蓝笔",num:1,price:8},
{goodsName:"毛笔",num:1,price:9}
])
//计算属性total
const totalPrice = computed(()=>{
return goodsList.reduce((prev,next)=>{
return prev + (next.num * next.price)
},0)
})
//加减按钮
const changeNum = (item,type) => {
if(!type && item.num > 1){
item.num--
}
if(type && item.num < 100){
item.num++
}
}
//删除按钮
const deteleBtn = (index) => {
goodsList.splice(index,1)
}
</script>
- 这里简单定义一个购物车的面板, HTML包含商品数量加减,删除商品操作,
- 首先定义一个商品数据goodsList, 并且循环展示出来,
- 加减操作函数是通用的changeNum = (item,type) => {},传入当前点击的item,和点击的是加还是减用type的true和false区分,
- 删除操作deteleBtn = (index) => {} 传入当前点击的index,将商品数据中第index个元素删除即可,
- 最后是总价格totalPrice,这里使用计算属性将商品数据goodsList中每个商品的单价×数量并相加得到
这里使用的是数组的reduce方法,这里简单讲一下reduce方法:
reduce方法相当于数组使用forEach遍历数组
goodsList.reduce((prev,next)=>{
return prev + (next.num * next.price)
},0)
-------------------------
let allPrice = 0
goodsList.forEach(item =>{
allPrice += (item.num * num.price)
})
reduce(function(){},0)
reduce支持两个参数,第一个是回调函数,第二个是初始默认值
我这里使用的是箭头函数
(prev,next)=>{
return prev + (next.num * next.price)
}
箭头函数中的两个参数分别是,prev上一次的值, next当前的值
reduce的第二个参数0是第一次遍历的时候没有上一次的值,所以这边用0来默认上一次的值是0防止报错
那么再来说一下这里的totalPrice不使用computed应该是怎么写的:
<script setup lang="ts">
import { ref,reactive,computed } from 'vue';
//商品数据
const goodsList = reactive([
{goodsName:"铅笔",num:1,price:5},
{goodsName:"黑笔",num:1,price:6},
{goodsName:"红笔",num:1,price:7},
{goodsName:"蓝笔",num:1,price:8},
{goodsName:"毛笔",num:1,price:9}
])
//total
const totalPrice = ref(0)
//加减按钮
const changeNum = (item,type) => {
if(!type && item.num > 1){
item.num--
}
if(type && item.num < 100){
item.num++
}
getTotalPrice() //修改价格相关属性时需要调用函数更新total的值
}
//删除按钮
const deteleBtn = (index) => {
goodsList.splice(index,1)
getTotalPrice() //修改价格相关属性时需要调用函数更新total的值
}
//获取total的值
const getTotalPrice = () => {
totalPrice.value = goodsList.reduce((prev,next)=>{
return prev + (next.num * next.price)
},0)
}
getTotalPrice() //页面加载先调用一次,计算出total
</script>
因为totalPrice这个属性是跟随着商品数量来变化的所以可能有多处地方可以改变,每次去调用函数重新计算值就会相对繁琐,所以可以直接使用计算属性可以有效地节省代码量。
1.2、Vue2和Vue3的区别
//vue2 选项式API
<script>
export default{
data(){
return{
goodsList:[
{goodsName:"铅笔",num:1,price:5},
{goodsName:"黑笔",num:1,price:6},
{goodsName:"红笔",num:1,price:7},
{goodsName:"蓝笔",num:1,price:8},
{goodsName:"毛笔",num:1,price:9}
]
}
},
computed:{
totalPrice(){
return this.goodsList.reduce((prev,next)=>{
return prev + (next.num * next.price)
},0)
}
},
methods:{
deteleBtn(index){
this.goodsList.splice(index,1)
},
changeNum(item,type){
if(!type && item.num > 1){
item.num--
}
if(type && item.num < 100){
item.num++
}
}
}
}
</script>
//vue3 组合式API
<script setup lang="ts">
import { ref,reactive,computed } from 'vue';
//商品数据
const goodsList = reactive([
{goodsName:"铅笔",num:1,price:5},
{goodsName:"黑笔",num:1,price:6},
{goodsName:"红笔",num:1,price:7},
{goodsName:"蓝笔",num:1,price:8},
{goodsName:"毛笔",num:1,price:9}
])
//计算属性total
const totalPrice = computed(()=>{
return goodsList.reduce((prev,next)=>{
return prev + (next.num * next.price)
},0)
})
//加减按钮
const changeNum = (item,type) => {
if(!type && item.num > 1){
item.num--
}
if(type && item.num < 100){
item.num++
}
}
//删除按钮
const deteleBtn = (index) => {
goodsList.splice(index,1)
}
</script>
二、watch
watch在Vue3中的写法
watch(监听的属性,callback(){},option)
watch支持三个参数
- 第一个参数 - 需要监听的属性
- 第二个参数 - 属性变化时调用的回调函数
- 第三个参数 - option配置,例如deep深度监听等配置属性
- 前两个参数是必须要有的,第三个参数看情况可有可无
监听常规类型变量
watch在Vue中的作用是监听属性变化,和computed很相似,当监听的属性发生变化时做出响应的处理,直接举个例子来清晰的看看watch的用法:
<template>
<div>
<input type="text" v-model="name">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let name = ref('orange')
watch(name,(newVal,oldVal)=>{
console.log(newVal,oldVal);
})
</script>
当input双向绑定name,当name改变时就会触发watch中的回调函数
监听引用类型变量
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = ref({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(obj,(newVal,oldVal)=>{
console.log(newVal,oldVal);
})
</script>
使用ref定义的引用类型变量(这里也就是obj对象),直接监听整个obj会发现修改name或者age都不触发watch的callback回调函数,因为这里监听的是整个obj对象,只有当整个obj被改变时才能监听的到,那要怎么样才能监听到呢?
// 1 使用watch的第三个参数, option配置中添加deep 深度监听
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = ref({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(obj,(newVal,oldVal)=>{
console.log(newVal,oldVal);
},{
deep:true //深度监听
})
</script>
// 2 使用reactive来定义变量, 不需要开启deep也能进行监听
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(obj,(newVal,oldVal)=>{
console.log(newVal,oldVal);
})
</script>
单独监听引用类型中其中一个属性
这里如果只想监听name属性, 当age发生改变时不触发callback
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(obj.foo.bar.name,(newVal,oldVal)=>{ //单独监听其中一个属性 这样写会报错
console.log(newVal,oldVal);
})
</script>
这里提示的意思是:没有重载与此调用匹配。
最后一个重载产生以下错误。
“string”类型的参数不可分配给“object”类型的变量。
watch的第一个参数需要的是object类型,不能是string类型,所以我们这里传一个箭头函数给他就可以了:
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(()=>{
return obj.foo.bar.name
},(newVal,oldVal)=>{
console.log(newVal,oldVal);
})
</script>
这样就可以对引用类型中的属性单独监听了
watch中的option
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(()=>{
return obj.foo.bar.name
},(newVal,oldVal)=>{
console.log(newVal,oldVal);
},{
deep:true, //深度监听
immediate:true, //立即执行一次
flush:"pre" //pre 组件更新之前调用 sync 同步执行 post 组件更新之后调用
})
</script>
- deep 深度监听,使用ref变量时使用
- immediate 在监听的值第一次加载就调用了一次,这时候oldVal还没有值所以是undefined
- flush 这个是决定回调在什么阶段的时候调用
Vue2和Vue3的区别
//vue2 选项式API
<script>
export default{
data(){
return{
obj:{
foo:{
bar:{
name:"orange",
age:20
}
}
}
}
},
watch:{
"obj.foo.bar.name":{
handler(newVal,oldVal){
console.log(newVal,oldVal);
},
deep:true, //深度监听
immediate:true, //立即执行一次
flush:"pre" //pre 组件更新之前调用 sync 同步执行 post 组件更新之后调用
},
}
}
</script>
//vue3 组合式API
<script setup lang="ts">
import { ref,reactive,computed, watch } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watch(()=>{
return obj.foo.bar.name
},(newVal,oldVal)=>{
console.log(newVal,oldVal);
},{
deep:true, //深度监听
immediate:true, //立即执行一次
flush:"pre" //pre 组件更新之前调用 sync 同步执行 post 组件更新之后调用
})
</script>
三、watchEffect
watchEffect的用法:
watchEffect((oninvalidata)=>{
obj.foo.bar.name //监听的值
obj.foo.bar.age //监听的值
console.log('监听:' , obj); //监听执行回调
oninvalidata(()=>{ //执行回调之前调用, 类似请求拦截器
console.log('before');
})
},option)
- watchEffect接收一个回调函数和一个option配置项
- 回调函数中直接写需要监听的属性, 只要出现在里面就会被监听,同时需要执行的回调内容也写在里面
- 回调函数支持一个参数oninvalidata,这个参数是一个函数,会在watchEffect回调执行前执行,类似请求拦截器
- option 也和watch一样拥有一些配置属性, 但是这个watchEffect运行时会自动调用一次,相当于开启了watch的immediate属性
运行时调用一次
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch, watchEffect } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watchEffect((oninvalidata)=>{
obj.foo.bar.name
obj.foo.bar.age
console.log('监听:' , obj);
})
</script>
oninvalidata的使用
<script setup lang="ts">
import { ref,reactive,computed, watch, watchEffect } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
watchEffect((oninvalidata)=>{
obj.foo.bar.name
obj.foo.bar.age
console.log('监听:' , obj);
oninvalidata(()=>{
console.log('before');
})
})
</script>
看到控制台会发现, 我改变了name的值, before先被打印了,所以oninvalidata函数中会先被执行, 可以用来做节流等一些操作
停止监听
<template>
<div>
姓名:<input type="text" v-model="obj.foo.bar.name">
<br>
年龄:<input type="text" v-model="obj.foo.bar.age">
<br>
<button type="submit" @click="stopWatch">停止监听</button>
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch, watchEffect } from 'vue';
let obj = reactive({
foo:{
bar:{
name:"orange",
age:20
}
}
})
const stop = watchEffect((oninvalidata)=>{
obj.foo.bar.name
obj.foo.bar.age
console.log('监听:' , obj);
oninvalidata(()=>{
console.log('before');
})
})
const stopWatch = () => stop()
</script>
停止监听先将watchEffect赋值给一个变量,然后再定义一个点击事件,在点击事件中调用这个函数即可,当点击停止监听后还会调用一次oninvalidata函数,监听内容将不再执行
watchEffect的option
来讲一讲option中的flush属性, watch和watchEffect中都有,一起讲
<template>
<div>
<div id="dom">这是一个dom元素</div>
</div>
</template>
<script setup lang="ts">
import { ref,reactive,computed, watch, watchEffect } from 'vue';
const stop = watchEffect((oninvalidata)=>{
const dom = document.querySelector('#dom')
console.log("dom=========>",dom);
},{
flush:"pre" //pre 组件更新之前调用 sync 同步执行 post 组件更新之后调用
})
const stopWatch = () => stop()
</script>
这里我们定义一个div,我们在watchEffect中获取该dom元素,并且来验证flush的不同值
- flush:"pre"
- pre是组件更新前调用watchEffect,组件更新前dom元素还没加载完成,所以这里的dom是null
- flush:"sync"
- sync是同步执行,这时候dom也未完全加载完毕所以也是null
- flush:"post"
- post是组件更新后调用,这时候dom已经加载完毕,所以dom不为null
watch和watchEffect同理,flush用的也比较少,只有具体需求是可能会用到,也先了解一下
四、总结
通过这篇文章已经大致了解到Vue3的computed计算属性和watch监听器的使用和写法,可以通过项目进行实战属性用法,该系列后续会继续更新学习Vue3过程中的知识点和难点进行分享。