Skip to content

计算属性-computed与侦听-watch(Lesson24 2/3) ​

本页对应 Lesson24,共 6 段视频,约 2 小时 12 分。

学习目标 ​

  • 能够分别使用 reactive 嵌套属性方案和 ref 方案重构 todoList,并验证新增、勾选、删除、清空选中项时视图能正确响应更新。
  • 能够使用组合式 API 注册 onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted 钩子,并通过日志或 DOM 操作验证各钩子的执行时机。
  • 能够使用 computed 创建只读计算属性,并通过缓存行为验证:依赖数据不变时计算函数不重复执行,依赖变化时才重新计算。
  • 能够使用 computed 的 get/set 创建可读写计算属性,并验证读取结果与设置值时对依赖数据的反向更新。
  • 能够使用 watch 监听 ref、reactive 对象、数组及函数返回值,正确使用 immediate、新旧值参数和停止监听函数,并说明 watchEffect 与 watch 在执行时机、依赖收集和获取旧值方面的区别。
  • 能够使用 watchEffect 自动收集依赖并立即执行回调,使用 onInvalidate 清理定时器等副作用,并能够通过模板 ref 获取普通 DOM 元素且在挂载完成后访问其真实 DOM。

知识点 ​

只读计算属性:先看三种写法 ​

为什么需要:模板里经常要展示“由多个响应式数据拼出来的结果”。如果每次都写拼接表达式,模板会变长;如果写成函数,又会在无关数据变化时重复执行。计算属性就是为“依赖明确、结果可缓存”的场景准备的。

怎么想:先写出依赖关系:userName 和 age 决定“今年某某几岁了”。再比较三种方案:函数调用、模板拼接、computed。转写稿里先写函数方案时,模板里把 getInfo() 大小写写错,页面报错;改正后,点击 age++、userName+='!' 都能更新。随后点击 count++,虽然 count 不影响结果,但视图更新时模板重新解析,getInfo() 仍会执行——这就是函数方案的浪费。

怎么做:从 Vue 引入 computed,用回调创建只读计算属性,模板中直接写属性名,不加括号。

易错点:computed 要引入;模板里计算属性不要写成 getInfoCom();函数方案才需要加括号。计算属性初次渲染会执行;模板里没使用它时不会执行;依赖不变时不会重复执行。

vue
<template>
    <h3>计算属性-只读</h3>
    <p @click="count++">count--->{{count}}</p>
    <p>userName--->{{userName}}</p>
    <p>age--->{{age}}</p>
    <p>方案一:函数-->{{getInfo()}}</p>
    <p>方案二:拼接-->{{"今年"+userName+","+age+"岁了"}}</p>
    <p>方案三:计算属性-->{{getInfoCom}}</p>
    <p>方案三:计算属性-->{{getInfoCom}}</p>
    <p>方案三:计算属性-->{{getInfoCom}}</p>
    <p>方案三:计算属性-->{{getInfoCom}}</p>
    <button @click="age++">点我更改age</button>
    <button @click="userName+='!'">点我更改userName</button>
    <hr/>
    <h3>计算属性-读写</h3>
    <p @click="count+=2">count-->{{count}}</p>
    <p>countCom->{{countCom}}</p>
    <button @click="countCom=300">修改计算属性的值</button>
</template>
<script setup lang="ts">
    import {ref,computed} from "vue";
    const count = ref(100);
    const age = ref(1);
    const userName = ref("zhangsan");

    // getInfo函数的返回结果,只受userName,以及age的影响,
    // 但是更新了数据count,由于模板中调用了该函数,所以该函数依然会被执行,哪怕结果没有任何变化。
    const getInfo = function(){
        console.log("getInfo");
        return "今年"+userName.value+","+age.value+"岁了"
    }
    // 计算属性:
    // 1- 引入组合API->computed
    // 2- computed接收一个回调函数
    // 3- 将computed函数的返回结果赋值给常量,变量
    // 4- 模板中可以直接渲染计算属性。
    // 5- 计算属性拥有缓存功能,只有所依赖的数据不发生改变,计算属性对应的函数不会再次执行。
    // 6- 模板中如果使用了计算属性,那么在初次渲染时,会执行。
    // 创建一个只读的计算属性(接收函数)
    const getInfoCom = computed(function(){
        console.log("getInfoCom")
        return "今年"+userName.value+","+age.value+"岁了"
    })
    // 创建一个读写的计算属性(对象)
    const countCom = computed({
        // 返回值即是计算属性的值
        get(){
            return count.value*2;
        },
        // 当设置计算属性值时调用。接收的参数是修改的值
        set(v:number){
           count.value = v/2;
        }
    })
</script>

缓存怎么验证 ​

为什么需要:计算属性的核心价值是缓存,不是“换一种写法”。

怎么想:转写稿中把 getInfoCom 在模板里连续写四遍,刷新后控制台只输出一次;点击 count++ 时也不输出;只有改 age 或 userName 才输出。说明它只依赖 age、userName,依赖不变就不重新计算。

怎么做:在计算属性回调里打 console.log,分别改无关数据和依赖数据观察输出。

易错点:不要以为模板写了四遍就会执行四次;也不要以为任何数据变化都会让计算属性重新执行。缓存成立的前提是依赖没有变化。

读写计算属性:get 与 set ​

为什么需要:有时希望“计算属性”既能读,也能被赋值。比如 countCom 永远等于 count * 2,点击按钮把 countCom = 300 时,应反推出 count = 150。

怎么想:只读计算属性传函数;读写计算属性传对象,里面写 get 和 set。get 的返回值就是计算属性的值;set(v) 在设置计算属性值时调用,v 是修改后的值。

怎么做:在 set 中写 count.value = v / 2。这样 countCom 变为 300 时,count 变成 150,再由 get 返回 300。

易错点:只读计算属性直接赋值会警告“写入失败”;读写计算属性必须用对象形式;set 参数要接住修改值,不要误以为 set 里拿到的是旧值。

先看完整示例:把“侦听”跑起来 ​

为什么需要:模板里 count、info 会变,但有些逻辑不是直接渲染,而是“数据变了我要做一件事”,例如打印日志、发请求、做校验。watch 就是用来侦听一个或多个响应式数据,数据变化后执行指定回调。

怎么想:先准备可变化的响应式数据,再写 watch(数据源, 回调, 配置);回调里观察 newValue、oldValue;需要手动停止时,保存 watch 的返回值并调用。

怎么做:保留下面代码,打开控制台,依次点击“更改 ref”“更改 reactive”“停止侦听”观察输出。

vue
<template>
    <h3>watch</h3>
    <p>ref->count->{{ count }}</p>
    <p>reactive->info->{{ info }}</p>
    <button @click="count++">更改ref->count</button>
    <br/>
    <button @click="info.age++">更改reactive->info->age</button>
    <button @click="info.userName+='!'">更改reactive->info->userName</button>
    <button @click="info.arr.push(info.arr.length+1)">更改reactive->info->arr</button>
    <button @click="info.arr[1]=90">更改reactive->info->arr->下标</button>
    <button @click="stopWatch">停止侦听</button>
</template>
<script setup lang="ts">
// watch:可以侦听一个或多个数据,如果侦听的数据发生变化,会执行指定的函数。
import {reactive, ref, watch} from "vue";
// count可以被称为:响应式ref对象|ref对象|ref
const count = ref(1);
// info可以被称为:响应式代理对象|响应式reactive对象|数据源代理对象
const info = reactive({
    userName: "zhangsan",
    age: 12,
    arr: [1, 2, 3, 4]
})
// 1- 侦听ref
// 第一个参数是侦听的数据。第二个参数是回调函数。第三个参数是一个配置对象。
// 当侦听的数据发生改变,那么会执行回调函数。
// watch的返回值是一个函数,通过调用该函数可以停止侦听(当数据发生改变,那么不会再次执行回调函数)。

// const stopWatch = watch(count,(newValue,oldValue)=> {
//     console.log("watch", newValue, oldValue)
// },{
//     // 立即调用,如果立即调用那么oldValue的值是undefined.
//     immediate:true
// })

// 2- 侦听数据代理对象
// const stopWatch = watch(info,(newValue,oldValue)=>{
//     console.log("侦听数据代理对象",newValue===oldValue)
// },{
//     immediate:true
// });

// 3- 侦听一个函数,该函数返回一个数据
// 函数要有一个返回值,返回值可以依赖一个或多个数据,当返回值发生改变以后会被侦听到。
// const stopWatch = watch(()=>count.value+info.age,(newValue,oldValue)=>{
//     console.log("侦听一个函数",newValue,oldValue)
// },{
//     immediate:true
// })

// 主要应用于侦听代理对象中的某个属性的变化
// 如果返回的是一个引用类型,引用类型的地址没有发生改变不会被侦听,如果被侦听需要增加deep:true
const stopWatch = watch(()=>info.age,(newValue,oldValue)=>{
    console.log("侦听一个函数",newValue,oldValue)
},{
    // deep:true,
    immediate:true
})

// 4- 侦听数组,数组内可以是所有的响应式数据
// newValue是更改后的数据,oldValue是更改前的数据。
// newValue以及oldValue类型是数组。数组中分别为对应数组的更改前以及更改后的值
// const stopWatch = watch([info,count],(newValue,oldValue)=>{
//     // newValue:[info更改后的值,count更改后的值]
//     // oldValue:[info更改前的值,count更改前的值]
//     console.log("侦听数组",newValue,oldValue)
// },{
//     immediate:true
// })
</script>

侦听 ref:最直观的“值变我执行” ​

为什么需要:ref 包装的基本类型最常见,count++ 后要触发额外逻辑。怎么想:ref 的值在脚本里是 count.value,但 watch(count, ...) 直接传 count 即可。怎么做:watch(count, (newValue, oldValue) => { ... });加 { immediate: true } 会在首次立即执行一次。易错点:立即执行时 oldValue 是 undefined;停止侦听要调用 watch 返回的函数,不是改数据。

侦听 reactive 代理对象:看整体,注意新旧值同一引用 ​

为什么需要:info 是一个对象,修改 age、userName、arr 都希望被感知。怎么想:watch(info, ...) 侦听的是这个代理对象整体。怎么做:点击“更改 reactive->info->age”等按钮,控制台会执行回调。易错点:回调里打印 newValue === oldValue 往往是 true,因为修改属性没有替换代理对象本身,新旧值指向同一个引用;不要误以为“没触发”。

侦听 getter 函数:精确侦听对象里的某个属性 ​

为什么需要:只想盯 info.age,不想 userName 变化也触发。怎么想:watch 第一个参数可以传一个函数,函数返回你要侦听的数据。怎么做:watch(() => info.age, (newValue, oldValue) => { ... });返回值依赖多个数据时,如 () => count.value + info.age,任一依赖变化都可能触发。易错点:如果 getter 返回引用类型且地址没变,默认不触发,需要按笔记注释加 deep: true;本例 info.age 是数字,不加 deep 也能触发。

侦听数组:一次盯多个数据源 ​

为什么需要:count 和 info 都要关注。怎么想:把多个数据源放进数组。怎么做:watch([info, count], (newValue, oldValue) => { ... }),newValue、oldValue 都是数组,位置与数据源对应。易错点:数组里的引用类型仍可能新旧同地址;调试时展开控制台对象看具体字段,不要只看 ===。

停止侦听与调试过程 ​

为什么需要:组件卸载或临时不需要时,避免回调继续执行。怎么想:watch 返回值是一个停止函数。怎么做:const stopWatch = watch(...),模板按钮 @click="stopWatch"。调试时点击各按钮,控制台应输出对应 newValue/oldValue;点击“停止侦听”后再改数据,不再输出。易错点:停止的是“侦听”,不是把数据删掉;immediate 的首次执行与后续变化要分开观察。

watchEffect 的基本定位:立即执行并自动收集依赖 ​

为什么需要:watch 往往要显式指定侦听源;当一段逻辑依赖多个响应式数据时,手动列清单容易漏。watchEffect 让回调先执行一次,并自动收集回调中读取到的响应式依赖。
怎么想:把它理解成“先跑一遍,边跑边记依赖;以后被记过的数据变了,就再跑”。
怎么做:在 App.vue 中,watchEffect 是组合式 API、是一个函数,接收回调且立即调用。

vue
// 一- 立即调用
// watchEffect(function(){
//     console.log("over");
// })

// 二- 回调函数中所依赖的数据发生改变,会执行回调函数。
const count = ref(1);
const obj = reactive({
    age:12
})
// 当count发生改变那么会执行
// watchEffect(()=>{
//     console.log(count.value);
// })

// 当count或obj.age发生改变,会执行回调函数。
// watchEffect(()=>{
//     console.log(count.value,obj.age);
// })

易错点:依赖必须在回调执行过程中被读取;没读到的数据不会被追踪。watchEffect 也拿不到修改前后的值,不要把它当 watch 用。

清除副作用:onInvalidate 与定时器清理 ​

为什么需要:回调里可能创建定时器、事件监听等“副作用”。依赖变化、手动停止、组件销毁时若不清理,会泄漏或叠加。
怎么想:在回调里注册一个清理函数,交给 onInvalidate;需要清理时它会被调用。
怎么做:Child.vue 中定时器每秒修改 num,并在 onInvalidate 中 clearInterval。

vue
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watchEffect} from "vue";
const num = ref(1);
// 应用
watchEffect(function(onInvalidate){
    console.log("watchEffect回调函数");
    const timer = setInterval(function(){
        console.log(1111);
        num.value++;
    },1000)
    onInvalidate(function(){
        console.log("onInvalidate");
        clearInterval(timer);
    })
})
</script>

调试推导:转写稿中反复点击“显示与隐藏”时,若没有清理定时器,控制台输出会越来越快,因为旧组件的定时器还在跑;只有最后一次加载的定时器控制当前显示状态,所以页面看似正常,实际计时器叠加。正确清理后,组件销毁时 onInvalidate 执行,定时器被清除。
易错点:onInvalidate 不会自动替你清定时器,必须手动 clearInterval;它在依赖变化、stop()、组件销毁时执行。

停止侦听:保存 stop 返回值 ​

为什么需要:某些条件满足后不再需要追踪,继续执行会浪费性能或产生副作用。
怎么想:watchEffect 的返回值是一个停止函数。
怎么做:

vue
// const stop = watchEffect(function(onInvalidate){
//     console.log(count.value,obj.age);
//     onInvalidate(function(){
//         console.log("清理副作用!")
//     })
// })
// setTimeout(()=>{
//     stop();
// },5000)

易错点:不保存返回值就无法停止;停止后回调不再因依赖变化执行,但停止时也会触发已注册的清理函数。

watchEffect 与 watch 的区别 ​

为什么需要:两者都能响应数据变化,但适用场景不同。
怎么想:按“执行时机、依赖来源、能否取旧值、清理副作用”四点比较。
怎么做:本节总结为:1)初次执行时机不同:watchEffect 立即执行,watch 默认不立即,需 immediate;2)watchEffect 回调中依赖的数据变化会执行,watch 是指定侦听数据变化才执行;3)watchEffect 无法获取修改前的值,watch 可以;4)watchEffect 拥有清理副作用函数。
易错点:不要因为 watchEffect 自动收集依赖,就以为它能精确告诉你“是哪个数据变了”;它只知道依赖集合中的某个数据触发。

完整示例:App.vue 与 Child.vue ​

vue
<template>
    <h3>watchEffect</h3>
    <p @click="count++">count-->{{count}}</p>
    <p @click="obj.age++">obj.age-->{{obj.age}}</p>
    <hr/>
    <button @click="isRender=!isRender">显示与隐藏</button>
    <Child v-if="isRender"/>
</template>
<script setup lang="ts">
import Child from "./components/Child.vue";
// 1- watchEffect是组合API
// 2- watchEffect是一个函数
// 3- watchEffect,接收一个回调函数,且该函数会被立即调用
import {onMounted, reactive, ref, watchEffect} from "vue";
const isRender = ref(true);
// 一- 立即调用
// watchEffect(function(){
//     console.log("over");
// })

// 二- 回调函数中所依赖的数据发生改变,会执行回调函数。
const count = ref(1);
const obj = reactive({
    age:12
})
// 当count发生改变那么会执行
// watchEffect(()=>{
//     console.log(count.value);
// })

// 当count或obj.age发生改变,会执行回调函数。
// watchEffect(()=>{
//     console.log(count.value,obj.age);
// })

// 三- 清除副作用函数
// 当所依赖的数据发生改变会执行清除副作用函数。
// 当停止时执行。
// 当组件被销毁时执行。
// 不需要指定侦听的数据(不知道)
// const stop =  watchEffect(function(onInvalidate){
//     console.log(count.value,obj.age);
//     onInvalidate(function(){
//         console.log("清理副作用!")
//     })
// })
// setTimeout(()=>{
//     stop();
// },5000)

// 总结:watch与watchEffect有何不同?
// 1- watchEffect与watch的初次执行时机不同。
// 2- watchEffect回调函数所依赖的数据发生改变会调用,watch是指定的侦听数据发生改变才会执行。
// 3- watchEffect无法获取修改前的值。watch可以。
// 4- watchEffect拥有清理副作用函数。
</script>
vue
<template>
<div>Child->{{num}}</div>
</template>

<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watchEffect} from "vue";
const num = ref(1);
// 应用
watchEffect(function(onInvalidate){
    console.log("watchEffect回调函数");
    const timer = setInterval(function(){
        console.log(1111);
        num.value++;
    },1000)
    onInvalidate(function(){
        console.log("onInvalidate");
        clearInterval(timer);
    })
})

// let timer = -1;
// onBeforeUnmount(function(){
//     clearInterval(timer);
// })
// onMounted(function(){
//     timer = setInterval(()=>{
//         num.value++;
//         console.log(11111)
//     },1000)
// })
</script>

<style scoped>

</style>

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:计算属性 computed(只读 / 读写) ​

src/App.vue

vue
<template>
  <h3>计算属性-只读</h3>
  <p @click="count++">count--->{{ count }}</p>
  <p>userName--->{{ userName }}</p>
  <p>age--->{{ age }}</p>
  <p>方案一:函数-->{{ getInfo() }}</p>
  <p>方案二:拼接-->{{ "今年" + userName + "," + age + "岁了" }}</p>
  <p>方案三:计算属性-->{{ getInfoCom }}</p>
  <p>方案三:计算属性-->{{ getInfoCom }}</p>
  <p>方案三:计算属性-->{{ getInfoCom }}</p>
  <p>方案三:计算属性-->{{ getInfoCom }}</p>
  <button @click="age++">点我更改age</button>
  <button @click="userName += '!'">点我更改userName</button>

  <hr />
  <h3>计算属性-读写</h3>
  <p @click="count += 2">count-->{{ count }}</p>
  <p>countCom->{{ countCom }}</p>
  <button @click="countCom = 300">修改计算属性的值</button>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(100);
const age = ref(1);
const userName = ref("zhangsan");

// 方案一:函数,视图每次更新都会被调用,哪怕结果没变
const getInfo = function () {
  console.log("getInfo");
  return "今年" + userName.value + "," + age.value + "岁了";
};

// 方案三:只读计算属性(接收一个回调函数)
const getInfoCom = computed(function () {
  console.log("getInfoCom");
  return "今年" + userName.value + "," + age.value + "岁了";
});

// 读写计算属性(接收一个带 get / set 的对象)
const countCom = computed({
  // 返回值即是计算属性的值
  get() {
    return count.value * 2;
  },
  // 当设置计算属性值时调用,接收的参数是修改的值
  set(v: number) {
    count.value = v / 2;
  },
});
</script>

要点:

  • 模板里使用计算属性 不加括号:{{ getInfoCom }};函数才写 {{ getInfo() }}。
  • getInfo 的返回值只依赖 userName、age,但点 count 时视图更新,函数仍会被执行一次——这是函数方案“浪费”的地方。
  • computed 有缓存:依赖不变,回调不再执行;模板中写 4 次 getInfoCom,初次渲染只执行 1 次;若模板中不使用,则一次都不执行。
  • 只读计算属性直接赋值会失败(Vue3 只给警告,值不会变),必须用 get/set 对象写法。
  • set(v) 里不要直接把 countCom 赋值,要改它依赖的源数据 count:count.value = v / 2,这样才能得到 countCom = 300 的效果。

示例二:侦听 watch 的四种用法 ​

src/App.vue

vue
<template>
  <h3>watch</h3>
  <p>ref->count->{{ count }}</p>
  <p>reactive->info->{{ info }}</p>
  <button @click="count++">更改ref->count</button>
  <br />
  <button @click="info.age++">更改reactive->info->age</button>
  <button @click="info.userName += '!'">更改reactive->info->userName</button>
  <button @click="info.arr.push(info.arr.length + 1)">更改reactive->info->arr</button>
  <button @click="info.arr[1] = 90">更改reactive->info->arr->下标</button>
  <button @click="stopWatch()">停止侦听</button>
</template>

<script setup lang="ts">
import { reactive, ref, watch } from "vue";

const count = ref(1);
const info = reactive({
  userName: "zhangsan",
  age: 12,
  arr: [1, 2, 3, 4],
});

// 1- 侦听 ref
const stopWatch1 = watch(
  count,
  (newValue, oldValue) => {
    console.log("watch ref", newValue, oldValue);
  },
  {
    // 立即调用,此时 oldValue 是 undefined
    immediate: true,
  }
);

// 2- 侦听响应式代理对象
const stopWatch2 = watch(
  info,
  (newValue, oldValue) => {
    console.log("侦听数据代理对象", newValue === oldValue);
  },
  { immediate: true }
);

// 3- 侦听一个函数,常用于侦听代理对象中的某个属性
const stopWatch3 = watch(
  () => info.age,
  (newValue, oldValue) => {
    console.log("侦听一个函数", newValue, oldValue);
  },
  { immediate: true }
);

// 4- 侦听数组,数组内可以是所有响应式数据
const stopWatch4 = watch(
  [info, count],
  (newValue, oldValue) => {
    // newValue / oldValue 都是数组,下标与侦听数组一一对应
    console.log("侦听数组", newValue, oldValue);
  },
  { immediate: true }
);

// watch 的返回值是一个函数,调用它即可停止侦听
const stopWatch = () => {
  stopWatch1();
  stopWatch2();
  stopWatch3();
  stopWatch4();
};
</script>

要点:

  • watch(数据源, 回调, 配置对象);返回值是函数,调用即停止侦听,停止后数据再变也不会执行回调。
  • immediate: true 时首次会立即执行一次,此时 oldValue 为 undefined。
  • 侦听 reactive 代理对象时,newValue === oldValue 为 true,因为改的是同一引用地址,只能靠 deep 或改成侦听函数解决。
  • 侦听函数 () => info.age 能精确侦听到某个基本类型属性;返回引用类型时地址不变不会被侦听,需要加 deep: true。
  • 侦听数组时,newValue、oldValue 是数组,第 i 项对应第 i 个数据源的新/旧值。
  • 易错点:同一个 const stopWatch = watch(...) 写多遍会重复声明报错,需分别命名(示例中用 stopWatch1~4)。

示例三:watchEffect(立即执行 + 清除副作用) ​

src/App.vue

vue
<template>
  <h3>watchEffect</h3>
  <p @click="count++">count-->{{ count }}</p>
  <p @click="obj.age++">obj.age-->{{ obj.age }}</p>
  <hr />
  <button @click="isRender = !isRender">显示与隐藏</button>
  <Child v-if="isRender" />
</template>

<script setup lang="ts">
import Child from "./components/Child.vue";
import { reactive, ref, watchEffect } from "vue";

const isRender = ref(true);
const count = ref(1);
const obj = reactive({
  age: 12,
});

// 一- 立即调用:watchEffect 接收的回调函数会被立即执行一次
// watchEffect(function () {
//   console.log("over");
// });

// 二- 回调函数中所依赖的数据发生改变,会再次执行回调函数
// watchEffect(() => {
//   console.log(count.value, obj.age);
// });

// 三- 清除副作用函数
// 当所依赖的数据发生改变会执行清除副作用函数;当停止时执行;当组件被销毁时执行
const stop = watchEffect(function (onInvalidate) {
  console.log(count.value, obj.age);
  onInvalidate(function () {
    console.log("清理副作用!");
  });
});

setTimeout(() => {
  stop();
}, 5000);
</script>

src/components/Child.vue

vue
<template>
  <div>Child->{{ num }}</div>
</template>

<script setup lang="ts">
import { ref, watchEffect } from "vue";

const num = ref(1);

// 应用:定时器在组件销毁 / 停止侦听时被清掉,避免定时器越积越多
watchEffect(function (onInvalidate) {
  console.log("watchEffect回调函数");
  const timer = setInterval(function () {
    console.log(1111);
    num.value++;
  }, 1000);
  onInvalidate(function () {
    console.log("onInvalidate");
    clearInterval(timer);
  });
});
</script>

要点:

  • watchEffect 不需要指定侦听数据源,回调里“用到谁就依赖谁”,用到的响应式数据变化就重新执行。
  • 与 watch 的区别:① 初次执行时机不同(watchEffect 立即执行,watch 要配 immediate);② watchEffect 由依赖自动收集,watch 是显式指定数据源;③ watchEffect 拿不到修改前的值;④ watchEffect 拥有清除副作用函数。
  • onInvalidate 的触发时机:依赖数据变化时、调用 stop() 停止时、组件被销毁时——正是清理 setInterval、事件监听的好地方。
  • 易错点:不清理定时器时,v-if 反复切换会不断新建计时器,数据越跳越快;不要在 onInvalidate 里再写业务逻辑,只做清理。
  • 子组件通过 import Child from "./components/Child.vue" 引入后,<script setup> 中无需注册即可直接在模板使用。

常见错误 ​

  1. 现象:给 computed(() => ...) 创建的计算属性直接赋值,控制台出现只读警告。
    原因:函数式 computed 默认是只读计算属性。
    修法:需要写回时改用 computed({ get(){...}, set(v){...} }),并在 set 中反推、修改真正的数据源。

  2. 现象:watch(() => info.age, ...) 能触发,但改成 watch(info, ...) 后改 info.age 看似“没反应”,或 newValue === oldValue。
    原因:侦听整个 reactive 代理时拿到的是同一引用;深层变化是否触发还受 deep 影响。
    修法:优先侦听具体字段 getter,如 () => info.age;确需整体侦听时加 deep: true,并不要依赖新旧值引用比较。

  3. 现象:watchEffect 里创建的定时器、事件监听在组件卸载后仍在执行。
    原因:副作用未清理,watchEffect 停止或组件销毁时没有释放资源。
    修法:使用 onInvalidate(() => clearInterval(timer)) 清理;需要手动停止时保存 stop 返回值并调用。

  4. 现象:模板 ref 绑定的 DOM 在 setup 顶层读取为 null 或初始值。
    原因:组件尚未挂载,真实 DOM 还没生成。
    修法:在 onMounted 后读取;v-if 场景要判空,必要时结合 nextTick。

  5. 现象:watch 配置 immediate: true 后,第一次回调里 oldValue 是 undefined,业务逻辑报错。
    原因:立即执行时没有“修改前值”。
    修法:回调内先判空,或不要在该场景依赖 oldValue。

  6. 现象:计算属性写了但没执行,误以为失效。
    原因:计算属性具有惰性,未在模板或其他响应式上下文中被使用时不会求值。
    修法:确认模板/渲染或其它计算属性中实际使用了它;不要用“是否打印”判断其存在性。

  7. 现象:watch 返回值没保存,组件卸载或条件切换后仍想停止侦听却停不掉。
    原因:watch 返回的是停止函数,未保存就无法主动调用。
    修法:用常量接收 const stop = watch(...),在合适时机调用 stop()。

  8. 现象:watchEffect 里访问了多个数据,以为它会侦听所有响应式数据。
    原因:只收集回调执行时实际同步访问到的依赖。
    修法:依赖不明确时改用 watch 显式指定数据源;使用 watchEffect 时保证依赖在回调中直接、同步访问。

练习 ​

  1. 下面代码中 doubleCount 是只读计算属性,点击按钮赋值会警告。请改为可写计算属性,并保证修改 doubleCount = 300 后 count 变为 150。

    ts
    const count = ref(100)
    const doubleCount = computed(() => count.value * 2)
    // 按钮:doubleCount = 300
  2. 现有 watch(info, ...) 侦听整个 reactive 对象,改 info.age++ 时新旧值相同且不易定位变化字段。请改成只侦听 info.age,并说明 immediate: true 时 oldValue 应如何处理。

  3. 子组件中用 watchEffect 创建了 setInterval,父组件通过 v-if 反复切换显示隐藏后,定时器越来越多。请写出用 onInvalidate 清理的改法,并说明手动 stop() 时清理函数是否执行。

  4. 模板中写 <input ref="userNameRef">,要求在组件挂载后读取输入框真实 DOM,并与 document.querySelector 结果做比较;如果输入框外层有 v-if,请补上判空处理。

  5. 用 watch([count, () => info.age], (newVal, oldVal) => {...}, { immediate: true }) 同时侦听一个 ref 和一个 reactive 字段。请写出 newVal、oldVal 的数组结构,并指出首次立即执行时各自的值。

原视频对照 ​

#视频时长
1todoList其它方案10:37
2生命周期组合式API27:03
3计算属性-computed27:43
4侦听-watch27:50
5watchEffect29:44
6ref与普通元素结合使用09:54

小结 ​

  1. 派生状态优先用计算属性;需要写回时用 get/set,并保持“单一数据源”,不要在 set 里制造第二份真相。

  2. 侦听 reactive 数据时,能盯具体字段就不要盯整个代理;引用类型的新旧值比较看地址,不要被 newValue === oldValue 误导。

  3. watch 适合依赖明确、需要旧值或需要精确控制的场景;watchEffect 适合依赖自动收集、副作用清理的场景,但依赖必须可读、可控。

  4. 生产代码里创建定时器、事件监听、请求取消等副作用,必须配套清理;onInvalidate、stop、组件卸载三种时机要区分清楚。

  5. 模板 ref 读 DOM 要放到挂载后;v-if 场景下 ref 可能为 null,读取前判空是硬要求。

  6. immediate: true 时不要假设 oldValue 有业务含义;首次执行逻辑应与后续变更逻辑分开考虑。

  7. 组合式生命周期中没有与 beforeCreate/created 对应的钩子,初始化逻辑直接写在 setup 顶层;挂载前后、更新前后、销毁前后的钩子按阶段使用。

  8. 停止侦听函数要保存;组件卸载或条件分支切换时,按需调用 stop(),避免“看不见的侦听”继续运行。