主题
计算属性-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>课堂画面






代码示例
示例一:计算属性 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>中无需注册即可直接在模板使用。
常见错误
现象:给
computed(() => ...)创建的计算属性直接赋值,控制台出现只读警告。
原因:函数式computed默认是只读计算属性。
修法:需要写回时改用computed({ get(){...}, set(v){...} }),并在set中反推、修改真正的数据源。现象:
watch(() => info.age, ...)能触发,但改成watch(info, ...)后改info.age看似“没反应”,或newValue === oldValue。
原因:侦听整个 reactive 代理时拿到的是同一引用;深层变化是否触发还受deep影响。
修法:优先侦听具体字段 getter,如() => info.age;确需整体侦听时加deep: true,并不要依赖新旧值引用比较。现象:
watchEffect里创建的定时器、事件监听在组件卸载后仍在执行。
原因:副作用未清理,watchEffect停止或组件销毁时没有释放资源。
修法:使用onInvalidate(() => clearInterval(timer))清理;需要手动停止时保存stop返回值并调用。现象:模板
ref绑定的 DOM 在setup顶层读取为null或初始值。
原因:组件尚未挂载,真实 DOM 还没生成。
修法:在onMounted后读取;v-if场景要判空,必要时结合nextTick。现象:
watch配置immediate: true后,第一次回调里oldValue是undefined,业务逻辑报错。
原因:立即执行时没有“修改前值”。
修法:回调内先判空,或不要在该场景依赖oldValue。现象:计算属性写了但没执行,误以为失效。
原因:计算属性具有惰性,未在模板或其他响应式上下文中被使用时不会求值。
修法:确认模板/渲染或其它计算属性中实际使用了它;不要用“是否打印”判断其存在性。现象:
watch返回值没保存,组件卸载或条件切换后仍想停止侦听却停不掉。
原因:watch返回的是停止函数,未保存就无法主动调用。
修法:用常量接收const stop = watch(...),在合适时机调用stop()。现象:
watchEffect里访问了多个数据,以为它会侦听所有响应式数据。
原因:只收集回调执行时实际同步访问到的依赖。
修法:依赖不明确时改用watch显式指定数据源;使用watchEffect时保证依赖在回调中直接、同步访问。
练习
下面代码中
doubleCount是只读计算属性,点击按钮赋值会警告。请改为可写计算属性,并保证修改doubleCount = 300后count变为150。tsconst count = ref(100) const doubleCount = computed(() => count.value * 2) // 按钮:doubleCount = 300现有
watch(info, ...)侦听整个 reactive 对象,改info.age++时新旧值相同且不易定位变化字段。请改成只侦听info.age,并说明immediate: true时oldValue应如何处理。子组件中用
watchEffect创建了setInterval,父组件通过v-if反复切换显示隐藏后,定时器越来越多。请写出用onInvalidate清理的改法,并说明手动stop()时清理函数是否执行。模板中写
<input ref="userNameRef">,要求在组件挂载后读取输入框真实 DOM,并与document.querySelector结果做比较;如果输入框外层有v-if,请补上判空处理。用
watch([count, () => info.age], (newVal, oldVal) => {...}, { immediate: true })同时侦听一个 ref 和一个 reactive 字段。请写出newVal、oldVal的数组结构,并指出首次立即执行时各自的值。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | todoList其它方案 | 10:37 |
| 2 | 生命周期组合式API | 27:03 |
| 3 | 计算属性-computed | 27:43 |
| 4 | 侦听-watch | 27:50 |
| 5 | watchEffect | 29:44 |
| 6 | ref与普通元素结合使用 | 09:54 |
小结
派生状态优先用计算属性;需要写回时用
get/set,并保持“单一数据源”,不要在set里制造第二份真相。侦听 reactive 数据时,能盯具体字段就不要盯整个代理;引用类型的新旧值比较看地址,不要被
newValue === oldValue误导。watch适合依赖明确、需要旧值或需要精确控制的场景;watchEffect适合依赖自动收集、副作用清理的场景,但依赖必须可读、可控。生产代码里创建定时器、事件监听、请求取消等副作用,必须配套清理;
onInvalidate、stop、组件卸载三种时机要区分清楚。模板
ref读 DOM 要放到挂载后;v-if场景下 ref 可能为null,读取前判空是硬要求。immediate: true时不要假设oldValue有业务含义;首次执行逻辑应与后续变更逻辑分开考虑。组合式生命周期中没有与
beforeCreate/created对应的钩子,初始化逻辑直接写在setup顶层;挂载前后、更新前后、销毁前后的钩子按阶段使用。停止侦听函数要保存;组件卸载或条件分支切换时,按需调用
stop(),避免“看不见的侦听”继续运行。