侦听器

@Watcher
Overview
watch 主要用于监听响应式数据的变化,并在变化时执行 副作用 操作 - allow you to observe changes in your reactive state and perform side effects accordingly.
监听响应式数据源;当数据源发送变化时,在回调函数中处理响应的逻辑
使用 watch 函数在每次响应式状态发生变化时触发回调函数
侦听的是响应式数据,不能直接侦听响应式数据的属性
三个:数据源 source、回调函数 callback、配置项 options
需引入包 watch;更多信息,请访问 侦听 - WatchersAPI - watch()
import { watch } from 'vue'
function watch<T>(
    source: WatchSource<T>,
    callback: WatchCallback<T>,
    options?: WatchOptions
): StopHandle
Source
侦听数据类型 - 4种
. 一个 ref 对象 (包括计算属性)
. 一个 reactive 对象
. 一个 getter 函数:一个有返回的函数,特别适合侦听对象中的某个字段数据
. 多个响应式数据源组成的数组
实际开发中,更多是使用函数 Getter 来侦听数据的变化
Callback
侦听时,回调函数会使用2个参数来区分新值 newValue 和旧值 oldValue
通常我们只关心新值 newValue,即:一个参数就够了
可以是异步函数 async-await
onCleanup:回调函数的可选参数,是一个函数,用来注册清理回调;清理回调会在该副作用下一次执行前被调用
Options
1 立即侦听 immediate
watch 默认是懒执行的:仅当数据源变化时,才会执行回调
有时,需要在源变化之前就开始执行回调:设置配置项 immediate为true
如购物车总价,每次进入页面时都先结算一次;此时新值是当前值,而旧值未定义;同初恋一样,无前任
2 深度侦听 deep
默认情况下,只侦听简单类型响应式对象,不能侦听对象的属性或子对象
侦听复合对象内部属性时,需要开启深度侦听:设置 deep 为true
深度侦听需要遍历被侦听对象中的所有嵌套的属性;当用于大型数据结构时,开销很大,请只在必要时才使用它
侦听 reactive 对象时,默认开启 deep,且无法关闭
停止侦听
在大多数情况下,你无需关心怎么停止一个侦听器
要手动停止一个侦听器,请调用 watch 返回的函数
const stopWatch = watch(source, callback)

// 当已不再需要该侦听器时:
stopWatch()
ref
单个简单数据
当单击执行增加函数 inc 时,Vue 可以检测到 num 的变化,从而做出反应;如商品数量增加或减少时,总价发生变化
const num = ref(0)
const inc = () =>{
    num.value++
}
watch(num, (newv, oldv) => {
    console.log('num old vs new', oldv, newv);
})
如果侦听 num.value,则会提示数据源无效
Invalid watch source:  0 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.at ... 
[] 分析下面代码的功能
const msg = ref('')
const str = ref('')
watch(msg, (newv, oldv) => {
    str.value = newv.split('').reverse().join('')
})
多个简单数据
任何一个数据发生变化都会被监听到;是或的关系,不是与的关系
以数组的形式 分别 表示多个数据源:1个数据源变成多个数据源
以数组的形式 分别 显示新值和旧值:1个新值变成多个新值;一个旧值变成多个旧值
[] 信用卡年费免除 - 首年免年费;当年消费达到一定金额(10000)或消费多少次(6),免除次年年费
const times = ref(0)
const sum = ref(0)
function incTimes() {
    times.value++
}
function incSum() {
    sum.value += 1000
}
watch([times, sum], ([new1, new2], [old1, old2]) => {
    console.log(old1, new1, old2, new2);
})
复合数据 - 对象型 Object
watch 只会监听对象的变化,而不会监听对象内部属性的变化
const user = ref({
  id: 100,
  name: 'glpla',
  age: 18,
  addr: {
      city: 'cq',
      code: '541000'
  }
})
对象属性 age 变化不会触发侦听
watch(user, (newv, oldv) => {
    console.log('user is watched', newv, oldv);
})
改进1:开启深度侦听:设置 deep为 true
属性变化可以触发侦听;但是变化前后都是同一个对象;所以 newv 和 oldv 是一样的,其内部的属性会变化;仅仅知道数据属性发生变化,无法获知前后变化情况
利用开发者视图查看新旧值,是一样的
watch(user, (newv, oldv) => {
    console.log('user is watched', newv, oldv);
}, { deep: true })
改进2:利用 函数 获取侦听属性 - getter 函数,仅仅当该属性变化时才触发监听
age 变化时,触发侦听;其它属性变化时,不触发
不要遗漏 value
watch(() => user.value.age, (newv, oldv) => {
  console.log('user age is watched', newv, oldv);
})
改进3:使用 reactive 类型的响应式数据
reactive
侦听 reactive 对象时,默认开启 deep,且无法关闭;所以对象某个属性变化时,会触发侦听/被 Vue 捕获
同样:新值旧值是 reactive 对象,变化前后都是同一个对象;所以 newv 和 oldv 是一样的,其内部的属性也是一样的;可以捕获到数据属性变化,但是无法查看属性前后的变化
const user = reactive({
  id: 100,
  name: 'glpla',
  age: 18,
  addr: {
    city: 'cq',
    code: '541000'
  }
})
任一属性变化时,可以触发侦听
watch(user, (newv, oldv) => {
  console.log('user is watched', newv, oldv);
  console.log('user is watched', newv.age, oldv.age);
})
单个属性变化
应用最为广泛
利用 函数 获取侦听的属性 - 简单属性
watch(() => user.id, (newv, oldv) => {
  console.log('user id is watched', newv, oldv);
})
利用 函数 获取侦听的属性 - 复合属性
watch(() => user.addr.code, (newv, oldv) => {
  console.log(newv, oldv);
})
多个属性变化
watch([() => user.age, () => user.addr.code], ([n0, n1], [o0, o1]) => {
  console.log(o0, n0, o1, n1);
})
Summary & Homework
Summary
侦听 ref()
侦听 reactive()
侦听 watch 常用于触发异步操作等副作用,比如根据用户输入去发起 API 请求获取数据
计算属性 computed 多用于数据的2次处理
可以侦听一个或多个响应式数据
可以使用 getter 函数侦听响应式数据的特定属性
Homework
[] 详情页 - 数量变化引起总价变化
方案1. 使用计算属性实现:1个响应式数据,1个派生数据;根据产品数量 num 派生总价 sum
import { ref, computed } from 'vue';
const num = ref(1);
const price = 9.9
const sum = computed(() => {
  return num.value * price;
});
方案2. 使用侦听实现:2个响应式数据;侦听产品数量 num,执行副作用 - 计算总价 sum
为什么要开启立即选项?
<div>{{ sum }}</div>
<div class="inc" @click="num++">inc</div>
<div>{{ num }}</div>
<div class="dec" @click="num--">dec</div>
import { ref, watch } from 'vue';
const num = ref(1);
const sum = ref(0);
const price = 9.9
watch(num, (newv, oldv) => {
  console.log('user is watched', newv, oldv);
  sum.value = newv * price;
}, { immediate: true })
[] 登录页 - 发送验证码
发送验证码:初始状态禁止 disabled
侦听手机号码,如何符合要求,则激活发送验证码按钮
表单其它数据和功能略
<div class="item">
  <input type="text" v-model.number="user.code" required maxlength="6">
  <button class="tips" :disabled="isCellActive" @click="getCode">获取验证码</button>
</div>
const user = ref({
  cell: '',
  code: null,
  isAgree: false,
  code: null
})
const isCellActive = ref(true)
watch(() => user.value.cell, (n, o) => {
  if (/^1[3-9]\d{9}$/.test(n)) {
    isCellActive.value = false
  } else {
    isCellActive.value = true
  }
})
[] 购物车
总价

侦听购物车商品长度或某个商品数量

选中某个商品,发生变化时,统计总价

修改选中商品的数量,更新总价

全选初判断

计算总价还要判断是否是全选;当选择了所有商品后,应为全选状态

继续选中商品,当选中商品的种类数量等于待选商品种类的数量时,应为全选

全选再判断

侦听购物车商品的全选对应的多选按钮 checkbox;选中 true,为全选;否则取消全选

[] 侦听仓库:仓库状态发生变化时,给出相应的提示
更多信息,请访问 仓库 - Pinia
[] 侦听模态框的变化
用户信息采集过程中,非法、无效等数据校验,前端可以使用弹出窗口给出提示;一定时间后弹出窗口消失
需要在每个逻辑的校验中,弹出窗口、消失窗口
可以侦听弹窗的变化,当出现|条件为真时,开始倒计时,然后停止倒计时,关闭弹窗
watch(isModalOpen, (newval, oldval) => {
  if (newval) {
    setTimeout(() => {
    isModalOpen.value = false
    }, 3000);
  }
})
[] 积分榜 - 菜单的变化触发数据加载
请完善样式 - 静态样式和选中样式
<div v-for="tab in tabs" :key="tab.id" @click="word = tab.data">{{ tab.label }}</div>
import { ref, watch } from 'vue';
const tabs = ref([{
  id: 1,
  label: '软工3班',
  data: '20240203.json',
}, {
  id: 2,
  label: '软工4班',
  data: '20240204.json',
}])
const word = ref(tabs.value[0].data)
watch(word, (newValue, oldValue) => {
  console.log(newValue);
  fetch(`https://glpla.github.io/utils/data/rank/${newValue}`)
    .then(res => res.json())
    .then(data => {
      console.log(data);
    })
}, { immediate: true })
[] 下拉菜单 <select> 的变化触发数据加载
思路同上
[] 瑞幸咖啡首页标签页 - 随享瑞幸、颜值水杯 - 的变化触发数据加载
思路同上