Vue3学习日志2--computed和watch

999 阅读7分钟

image.png

前言

说明:本系列文章会持续更新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>

image.png

现在每一个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>

SDGIF_Rusult_1.gif

  • 这里简单定义一个购物车的面板, 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>

image.png 当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>

image.png

单独监听引用类型中其中一个属性

这里如果只想监听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>

image.png 这里提示的意思是:没有重载与此调用匹配。 最后一个重载产生以下错误。 “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>

image.png 这样就可以对引用类型中的属性单独监听了

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>

image.png

  • 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运行时会自动调用一次,相当于开启了watchimmediate属性

运行时调用一次

<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>

image.png

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>

image.png 看到控制台会发现, 我改变了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>

image.png 停止监听先将watchEffect赋值给一个变量,然后再定义一个点击事件,在点击事件中调用这个函数即可,当点击停止监听后还会调用一次oninvalidata函数,监听内容将不再执行

watchEffect的option

来讲一讲option中的flush属性, watchwatchEffect中都有,一起讲

<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

image.png

  • flush:"sync"
  • sync是同步执行,这时候dom也未完全加载完毕所以也是null

image.png

  • flush:"post"
  • post是组件更新后调用,这时候dom已经加载完毕,所以dom不为null

image.png

watch和watchEffect同理,flush用的也比较少,只有具体需求是可能会用到,也先了解一下

四、总结

通过这篇文章已经大致了解到Vue3的computed计算属性和watch监听器的使用和写法,可以通过项目进行实战属性用法,该系列后续会继续更新学习Vue3过程中的知识点和难点进行分享。

系列视频

上一集:Vue3学习日志1--初识Vue3
下一集:Vue3学习日志3--组件和生命周期