主题
切换-侦听与切换-计算属性(Lesson6 2/3)
本页对应 Lesson06,共 5 段视频,约 58 分钟。
学习目标
- 能使用
watch监听index等数据变化,并同步更新派生数组,完成选项卡切换内容渲染。 - 能使用
computed实现相同的切换、搜索与全名拼接功能,并说明其与watch在返回值、缓存、初始调用和异步支持上的区别。 - 能独立实现关键字搜索过滤,使用
includes完成大小写兼容的数组筛选,并分别用watch与computed完成同一需求。 - 能实现
firstName与lastName的全名拼接,并根据场景选择watch或computed方案。 - 能使用
vm.$watch在实例外部监听数据变化,正确传入监听字段、回调函数及deep配置完成深度侦听。 - 能使用
vm.$el获取 Vue 实例挂载的真实根元素,并说明其与document.querySelector获取元素的关系。
知识点
完整示例与本节目标
为什么需要:切换新闻分类时,如果每次都手动操作 DOM,代码会又长又难维护;Vue 里可以用数据驱动视图,把“当前选中项”和“要显示的列表”交给数据状态管理。
怎么想:把分类数据 newsList 写死,用 index 记录当前选中的分类;按钮点击时只改 index,再用 watch 监听 index,把对应分类的 infoList 赋给 infoArr,模板只渲染 infoArr。
怎么做:完整示例保留如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#app button{
padding:5px;
margin:5px;
}
#app button.active{
background: red;
}
#app div{
width: 500px;
height: 300px;
border:1px solid green;
}
</style>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button :class="{active:index===i}" @click="index=i" v-for="(item,i) in newsList" :key="i">{{item.type}}</button>
<div>
<p v-for="item in infoArr" :key="item.id">{{item.newsTitle}}</p>
</div>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
index:1,
infoArr:[],
newsList:[
{
type:"体育",
infoList:[
{
id:11,
newsTitle:"体育1"
},
{
id:12,
newsTitle:"体育2"
}
]
},
{
type:"娱乐",
infoList:[
{
id:21,
newsTitle:"娱乐1"
},
{
id:22,
newsTitle:"娱乐2"
}
]
},
{
type:"财经",
infoList:[
{
id:31,
newsTitle:"财经1"
},
{
id:32,
newsTitle:"财经2"
}
]
}
]
},
watch:{
// index(){
// this.infoArr = this.newsList[this.index].infoList;
// }
index:{
handler(){
this.infoArr = this.newsList[this.index].infoList;
},
immediate:true
}
}
})
</script>
</html>易错点:index 初始值是 1 时默认显示“娱乐”;如果改成 0,默认显示“体育”。按钮的 active 类要绑定 index===i,否则点了没有高亮。v-for 的 :key 不要漏。
切换为什么用侦听
为什么需要:点击按钮只改变 index,但下面列表不能自动变成对应分类,需要“当 index 变化后做一件事”。
怎么想:watch 适合监听一个已有数据变化后的行为,这里监听 index,在回调里重新给 infoArr 赋值。
怎么做:先写简写形式 index(){ this.infoArr = this.newsList[this.index].infoList; },点击体育、娱乐、财经时 index 变化,侦听函数执行,infoArr 被替换,模板重新渲染。
易错点:watch 回调里不要忘记 this 指向 Vue 实例;infoArr 初始是空数组,所以不写 immediate 时刷新页面下面不显示内容。
immediate 解决首次不执行
为什么需要:watch 默认不会在页面初始化时立即调用,只有被监听的数据变化后才执行,因此刷新后 infoArr 仍是空数组。
怎么想:首次也需要执行一次“根据 index 取列表”的逻辑,所以把 watch 写成对象形式,加 handler 和 immediate:true。
怎么做:index:{ handler(){ this.infoArr = this.newsList[this.index].infoList; }, immediate:true }。这样刷新后立即执行一次,默认分类内容出现;点击按钮后继续执行。
易错点:immediate 是写在监听对象里的配置项,不是写在 data 里;handler 名字不要拼错。转写稿里“真听”就是“侦听/监听”,“音带”就是 index。
侦听与计算属性的取舍
为什么需要:同样的切换、搜索、全名拼接,watch 和 computed 都能做,但写法与适用场景不同。
怎么想:watch 偏向“数据变化后执行行为”,不需要返回值,支持异步;computed 偏向“由多个数据计算出一个结果”,必须有 return,有缓存,模板中使用才会执行,不支持异步。
怎么做:切换案例用 watch 监听 index 后改 infoArr;搜索案例可监听 keyword 生成 resultArr,也可用计算属性 resultArr(){ return this.arr.filter(...) };全名案例中 firstName、lastName 两个数据影响 fullName,用计算属性更简单,watch 则要分别监听两个数据。
易错点:计算属性不写 return 会得到 undefined;计算属性里写异步 setTimeout 不行;watch 不需要 return。如果只是根据多个数据算结果,优先考虑计算属性;如果是数据变化后做动作,优先考虑 watch。
调试过程与易错点汇总
为什么需要:转写稿里先出现“点击没效果、下面不更新”的情况,需要定位是数据没变、模板没用上,还是监听没触发。
怎么想:先看按钮点击后 index 是否变化,再看 active 是否绑定,最后看 infoArr 是否被赋值。
怎么做:给按钮加 @click="index=i" 和 :class="{active:index===i}";确认 watch 监听的是 index;加 immediate:true 解决首次不渲染;外部补充 vm.$watch('name', function(newValue, oldValue){}) 可监听数据,深度监听对象要传第三个参数 {deep:true};vm.$el 是 Vue 挂载的真实元素,可用于外部操作 DOM。
易错点:$watch 第一个参数是数据名,第二个是回调;监听对象属性变化不加 deep 不触发;$el 是挂载元素,不是虚拟 DOM;不要把所有逻辑都堆到 watch,能用计算属性表达结果时优先用计算属性。
场景与目标
本节要做一个“新闻分类切换”:三个按钮“体育 / 娱乐 / 财经”,点击后按钮高亮,下方显示对应分类的新闻列表。笔记要求用计算属性完成,同时对比侦听。转写稿里出现的“真听 / 钉钉”指 watch,“基酸属性 / 基成属性”指 computed,下面统一纠正为“侦听”和“计算属性”。
完整示例代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#app button{
padding:5px;
margin:5px;
}
#app button.active{
background: red;
}
#app div{
width: 500px;
height: 300px;
border:1px solid green;
}
</style>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button :class="{active:index===i}" @click="index=i" v-for="(item,i) in newsList" :key="i">{{item.type}}</button>
<div>
<p v-for="item in infoArr" :key="item.id">{{item.newsTitle}}</p>
</div>
</div>
</body>
<script>
// 侦听与计算属性的区别:
// 1- 侦听是通过watch声明,计算属性是通过computed声明
// 2- 侦听不需要返回值,计算属性要有返回值。
// 3- 侦听默认不会立即调用。计算属性在模板中调用才会执行
// 4- 侦听没有缓存,计算属性有缓存。
// 5- 侦听的应用:侦听某一数据发生改变后的一个行为。计算属性:受多个属性的变化而变化。
// 6- 侦听支持异步,计算属性不支持异步。
new Vue({
el:"#app",
data:{
index:1,
newsList:[
{
type:"体育",
infoList:[
{
id:11,
newsTitle:"体育1"
},
{
id:12,
newsTitle:"体育2"
}
]
},
{
type:"娱乐",
infoList:[
{
id:21,
newsTitle:"娱乐1"
},
{
id:22,
newsTitle:"娱乐2"
}
]
},
{
type:"财经",
infoList:[
{
id:31,
newsTitle:"财经1"
},
{
id:32,
newsTitle:"财经2"
}
]
}
]
},
computed:{
infoArr(){
return this.newsList[this.index].infoList;
}
}
})
</script>
</html>先拆数据:index 和 infoArr
为什么需要:切换的本质是“当前选中哪个分类”。如果按钮和列表都写死,就无法根据点击变化。
怎么想:newsList 保存全部分类,每项有 type 和 infoList;index 保存当前选中项下标;infoArr 是当前要渲染的列表。
怎么做:按钮用 v-for="(item,i) in newsList",高亮条件写 :class="{active:index===i}",点击写 @click="index=i";下方用 v-for="item in infoArr"。
易错点:高亮条件不要写成 item===i;key 用 i 或唯一 id;初始 index:1 时默认显示“娱乐”,要与预期一致。
转写稿里的调试过程很典型:一开始只让按钮变色,下面列表不变,原因是 index 还没参与下方渲染;把 index 用起来后,按钮和内容才产生响应关系。
计算属性方案
为什么需要:infoArr 不是手动维护的数据,而是由 index 和 newsList 共同“算出来”的结果,适合用计算属性。
怎么想:选中哪个分类,就返回哪个分类的 infoList:this.newsList[this.index].infoList。
怎么做:在 computed 中声明 infoArr(){ return this.newsList[this.index].infoList; },模板直接使用 infoArr。
易错点:计算属性必须有 return;不能写成 async;模板中不调用就不执行;有缓存,依赖不变不会重复执行;index 越界会报错。
转写稿中直接输出 infoArr 时发现“没执行”,因为模板还没用到它;在模板里写 {{infoArr}} 或 v-for 后刷新才执行一次。写两个相同插值也只执行一次,说明计算属性有缓存。
侦听方案与调试过程
为什么需要:有时数据变化后要执行一个行为,比如监听 index 改变后重新给 infoArr 赋值,这就是 watch 的典型应用。
怎么想:data 中先准备 infoArr: [];watch 监听 index,回调参数 newValue 是最新下标,把 this.newsList[newValue].infoList 赋给 this.infoArr。
怎么做:watch:{ index(newValue){ this.infoArr=this.newsList[newValue].infoList; } }。
易错点:侦听默认不会立即调用,刷新后页面可能空白,需要 immediate:true;侦听不需要返回值;侦听没有缓存;侦听支持异步,但计算属性不支持异步。
转写稿中点击按钮有效,但刷新没内容,就是因为默认不立即执行;加上 immediate:true 后才默认显示。
侦听与计算属性的区别
笔记注释已给出 6 条:声明方式不同;侦听不需要返回值,计算属性要有返回值;侦听默认不会立即调用,计算属性在模板中调用才执行;侦听没有缓存,计算属性有缓存;侦听偏向“某一数据改变后的行为”,计算属性偏向“受多个属性变化而变化的结果”;侦听支持异步,计算属性不支持异步。
易错点:不要为了用计算属性而写 async;需要“变化后做事”用侦听,需要“根据多个数据算结果”用计算属性;本切换两种都能做,但计算属性写法更简洁。
从“输入关键词就筛选”理解需求
搜索功能的本质是:输入框里的关键词变化后,从原数组 arr 中筛出包含该关键词的项,再把结果交给模板渲染。为什么需要侦听?因为关键词 keyword 是响应式数据,它一变,我们就希望执行“过滤”这个动作。怎么想?把“输入”和“筛选”分开:v-model 负责收集关键词,watch 负责在关键词变化时生成结果数组 resultArr,模板只负责遍历 resultArr。怎么做?在 data 中准备 keyword、resultArr、arr,其中 resultArr 初始为空数组。易错点是:不要把筛选逻辑全塞进模板;resultArr 要有明确初始值,否则模板遍历会报错或不可控。
先看非侦听写法:v-show 直接判断
笔记中保留了一段非侦听写法:
html
<!--非侦听-->
<!-- <p v-show="item.includes(keyword) && keyword" v-for="(item,index) in arr" :key="index">{{item}}</p>-->为什么需要先看它?因为它能帮助理解筛选条件。怎么想?直接遍历 arr,用 item.includes(keyword) 判断当前项是否包含关键词;后面的 && keyword 是为了避免关键词为空时 includes("") 返回 true,导致所有项都显示。怎么做?把这段作为对照即可。易错点有两个:第一,空字符串会被任何字符串包含,所以只写 includes(keyword) 会在初始状态显示全部;第二,includes 区分大小写,Z 不能匹配 zhangsan,需要统一大小写。
改用侦听:keyword 变化后生成 resultArr
为什么改用侦听?因为模板里写判断会让模板承担过多逻辑;用 watch 可以把“关键词变化 → 过滤数组”的过程放到 JS 中,模板只渲染 resultArr。怎么想?监听 keyword,回调拿到新值 newValue,用 arr.filter() 返回新数组,再赋值给 this.resultArr。怎么做?完整示例代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-model="keyword" type="text">
<!--非侦听-->
<!-- <p v-show="item.includes(keyword) && keyword" v-for="(item,index) in arr" :key="index">{{item}}</p>-->
<!--侦听-->
<p v-for="(item,index) in resultArr" :key="index">{{item}}</p>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
keyword:"",
resultArr:[],
arr:["zhangsan","lisi","wangwu","zhaoliu","yanqi","shenba","qianjiu","shenshi"]
},
watch:{
keyword(newValue){
this.resultArr = this.arr.filter(v=>v.includes(newValue.toLowerCase()));
}
}
})
</script>
</html>这段代码的关键是 filter:它不会修改原数组,而是返回一个满足条件的新数组,所以必须写 this.resultArr = ...。newValue.toLowerCase() 把输入统一成小写,才能和 arr 中的小写名字匹配。易错点:watch 回调里的 this 指向 Vue 实例;不要写成 resultArr = ... 漏掉 this;filter 的回调要 return 布尔值,箭头函数 v=>v.includes(...) 是简写。
调试过程:初始为空、输入 z、大小写匹配
转写稿里的调试思路可以直接还原:初始 resultArr 是空数组,所以页面一开始不渲染任何名字;输入 z 后,keyword 变化触发 watch,filter 从 arr 中筛出包含 z 的项,页面显示 zhangsan、zhaoliu;输入大写 Z 时,如果直接 includes("Z") 会匹配不到,所以代码里用 newValue.toLowerCase() 统一成小写。这里要注意:侦听写法初始为空,但如果你先输入再清空关键词,newValue 变成空字符串,includes("") 会让所有项都满足,resultArr 又会变成全部。若希望清空关键词时隐藏列表,需要在 watch 中先判断空值,例如清空 resultArr 后 return。这是从代码行为推导出的易错点,实际项目要按需求处理。
watch 与计算属性的取舍
为什么还要提计算属性?因为搜索也可以写成 computed:由 keyword 和 arr 算出结果。怎么想?watch 更偏向“某个数据变化后执行一个行为”,不要求返回值;computed 更偏向“由依赖直接算出一个结果”,必须有 return,并且有缓存。怎么做?本节用 watch 是为了练习侦听:keyword 变,执行过滤,把结果存到 resultArr。易错点:watch 默认不会在初始时立即调用;computed 如果不在模板中使用,就不会执行。搜索这种“由关键词直接算出列表”的场景,用计算属性往往更简洁;但用 watch 也完全能实现,关键是理解数据变化后的处理流程。真实列表的 :key 最好用唯一 id,示例中用 index 只是简化写法。
需求与数据准备
转写稿里多次把“侦听”说成“真听/钉听”,把“计算属性”说成“基算属性/计生属性”,这里统一纠正为:侦听 watch、计算属性 computed。本节要做的是一个搜索框:输入关键词,下面只显示名字中包含该关键词的项。
为什么需要:如果每次输入都手动遍历数组、拼接结果,逻辑会散落在事件里,输入框一变就要重复处理。搜索本质是“由关键词和原数组派生出一个结果数组”,适合交给计算属性。
怎么想:先准备两个数据:keyword 保存输入内容,arr 保存原始姓名列表。模板中让输入框绑定 keyword,列表循环 resultArr。resultArr 不放进 data,而是放进 computed,由 keyword 和 arr 计算出来。
怎么做:本节笔记代码如下,保留原代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input v-model="keyword" type="text">
<p v-for="(item,index) in resultArr" :key="index">{{item}}</p>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
keyword:"",
arr:["zhangsan","lisi","wangwu","zhaoliu","yanqi","shenba","qianjiu","shenshi"]
},
computed:{
resultArr(){
if(this.keyword)//zha;lskdjf;laskdjf
return this.arr.filter(v=>v.includes(this.keyword));
}
}
})
</script>
</html>易错点:v-model 绑定的是 keyword,不要写成 keyWord 或 word;v-for 循环的是 resultArr,不是 arr;:key="index" 只是示例写法,真实列表若有唯一 id 应优先用 id。
计算属性如何完成过滤
为什么需要:转写稿里先演示了“模板里 v-for 加 includes 判断”的传统方案,又演示了“侦听 keyword 后给 resultArr 赋值”的方案。前者把过滤逻辑写进模板,后者要额外维护一个结果数据。计算属性把“结果”直接声明出来,更集中。
怎么想:resultArr 的依赖只有两个:this.keyword 和 this.arr。只要关键词变化,Vue 会重新执行 resultArr;执行时用 filter 保留满足条件的项,用 includes 判断字符串是否包含关键词。
怎么做:在 computed 中写 resultArr(),内部 return this.arr.filter(v => v.includes(this.keyword))。模板里直接 v-for="(item,index) in resultArr",每项渲染 {{item}}。
易错点:includes 是区分大小写的,输入 Z 不一定能匹配 zhangsan;若要忽略大小写,需要把两边统一转小写后再比较。转写稿在侦听方案里提到过 toLowerCase(),计算属性方案同理。另一个易错点是空关键词:当前代码有 if(this.keyword),关键词为空时函数没有返回值,resultArr 为 undefined,列表不会渲染。若希望空关键词显示全部,应去掉 if,直接返回过滤结果;或显式 return this.arr。
调试过程与方案对比
为什么需要:转写稿中搜索功能先用了模板过滤,再用侦听实现,最后用计算属性实现。对比之后才能理解“什么时候用 computed 更顺”。
怎么想:侦听方案需要额外准备 resultArr,在 watch.keyword 里重新赋值;计算属性方案不需要额外数据,也不需要手写赋值语句。调试时先看输入框是否更新 keyword,再看 resultArr 是否随输入变化,最后看 v-for 是否拿到数组。
怎么做:打开页面后输入 zha,应只剩包含 zha 的项;清空输入时观察列表是否消失,这是当前 if 写法的直接结果。若列表不变化,依次检查:v-model 是否绑定正确、computed 名称是否与模板一致、filter/includes 是否写错、是否被 if 条件挡住。
易错点:不要把 computed 写成 methods;计算属性要有返回值,当前分支中空关键词没有返回值就是调试重点。转写稿还提到计算属性有缓存,模板中多次使用同一计算属性不会重复执行,但依赖变化会重新计算;这不等同于“永远不执行”。
本节要完成的功能
本节用一个小例子对比 Vue 中“侦听”和“计算属性”的用法:页面上有两个输入框,分别输入姓和名,下面把姓和名拼成全名显示出来。笔记代码中同时保留了两种实现:fullName 用 watch 维护,fullName2 用 computed 计算。
为什么需要这个例子?因为“全名”不是用户直接输入的数据,而是由 firstName 和 lastName 派生出来的结果。只要姓或名任意一个发生变化,全名就应该跟着变。怎么想?先准备两个源数据,再准备一个展示结果;怎么做?用 v-model 把两个输入框分别绑定到 firstName、lastName,模板里同时显示 {{fullName}} 和 {{fullName2}}。易错点是:fullName 不要当成用户输入字段去手动改,它应该由姓和名推导出来。
本节代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="firstName" placeholder="请输入姓">+
<input type="text" v-model="lastName" placeholder="请输入名">
{{fullName}}|{{fullName2}}
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
firstName:"",
lastName:"",
fullName:""
},
watch:{
firstName(){
this.fullName = this.firstName+this.lastName;
},
lastName(){
this.fullName = this.firstName+this.lastName;
}
},
computed:{
fullName2(){
return this.firstName+this.lastName;
}
}
})
</script>
</html>用 watch 侦听姓和名
为什么需要 watch?因为当某个数据变化后,我们可能想执行一段逻辑,比如这里要重新给 fullName 赋值。怎么想?fullName 依赖两个源:firstName 和 lastName,所以不能只盯一个。怎么做?在 watch 中分别写 firstName() 和 lastName(),两个函数里都执行:
js
this.fullName = this.firstName + this.lastName;这样无论改姓还是改名,都会重新拼接。
转写稿里的调试过程很关键:一开始只写了 firstName 的侦听,输入姓时右边会变;但再改“名”时,右边没有更新,因为 lastName 变化没有触发任何逻辑。于是补上 lastName 的侦听,两个入口都维护 fullName,效果才完整。易错点:只监听一个字段,是本节最常见的错误;另外 watch 默认不会在初始化时立即调用,所以 fullName 初始是 data 里的空字符串,若希望初始就执行,可以了解 immediate 配置,但本节代码没有使用。
用 computed 计算全名
为什么还需要计算属性?因为“全名”本质上是根据 firstName 和 lastName 算出来的结果,用计算属性更直接。怎么想?把 fullName2 写成一个函数,函数里返回两个数据的拼接;Vue 会自动追踪它依赖了谁。怎么做?
js
computed:{
fullName2(){
return this.firstName+this.lastName;
}
}模板中写 {{fullName2}},姓或名任意一个变化,fullName2 都会重新计算。
易错点:计算属性必须有 return,没有返回值就拿不到结果;计算属性是在模板中真正使用它时才执行,转写稿中刷新页面后观察过:模板里用了 fullName2,它才执行;并且它有缓存,同样依赖不变时不会重复计算。计算属性里不要做异步请求或修改其他状态,它适合“算结果”,不适合“做行为”。
watch 与 computed 的区别
为什么要把两者放一起?因为全名拼接用两种方式都能做,但写法不同。怎么想?watch 更偏向“监听某个数据变化后执行一段行为”,它不需要返回值;computed 更偏向“根据已有数据算出一个结果”,必须有返回值。怎么做?如果只是拼接全名,优先用 computed,代码更短,也不用同时监听两个字段;如果数据变化后要做请求、操作 DOM 等副作用,用 watch 更合适。
易错点:watch 做全名时要监听两个源,漏一个就不同步;computed 不要漏 return;computed 有缓存,依赖不变不会重新执行;转写稿还提到 watch 支持异步行为,而 computed 不支持异步,因为计算属性要同步返回结果。把这一点记住,面试和实际开发中都能判断该用谁。
先厘清:watch 与 computed 不是“谁替代谁”
这一节围绕 Vue 实例上的属性与方法展开,重点落在两类能力:一类是 watch 侦听,一类是 computed 计算属性。转写稿里反复比较二者,是因为很多需求两种写法都能完成,但写法、缓存、异步支持不同。为什么要先比较?因为如果只会“能跑”,遇到搜索、全名拼接、切换 tabs 时就容易选错方案。怎么想?先判断需求是“数据变了以后做一件事”,还是“由已有数据算出一个结果”。怎么做?前者优先 watch,后者优先 computed。易错点是:watch 默认不会立即调用,computed 必须有返回值且依赖模板使用才执行。
切换案例:watch 监听 index,computed 返回结果
为什么需要切换案例?它能把“点击改变 index,再改变下方内容”的过程看清楚。怎么想?准备 newsList,每项有 id、title、infoList;再准备 index,体育为 0、娱乐为 1、财经为 2。用 watch 时,监听 index,把对应分类的 infoList 赋给 infoArr:
js
data() {
return {
index: 0,
infoArr: [],
newsList: [
{ id: 1, title: '体育', infoList: [{ id: 11, newsTitle: '体育一' }] },
{ id: 2, title: '娱乐', infoList: [{ id: 21, newsTitle: '娱乐一' }] },
{ id: 3, title: '财经', infoList: [{ id: 31, newsTitle: '财经一' }] }
]
}
},
watch: {
index: {
immediate: true,
handler(newVal) {
this.infoArr = this.newsList[newVal].infoList
}
}
}模板里按钮用 v-for 渲染,点击时 index = i,下方 v-for="item in infoArr"。转写稿调试时发现“点击没效果”,原因就是 index 没真正参与响应或没被使用;把 index 用起来后按钮高亮和内容切换才正常。易错点:watch 不写 immediate 时,刷新页面不会给 infoArr 初始值,所以首次看不到内容。
用 computed 则不必维护 infoArr,直接返回结果:
js
computed: {
infoArr() {
return this.newsList[this.index].infoList
}
}为什么这样更简单?因为 infoArr 的结果依赖 index 和 newsList,任一变化都会重新计算。易错点:computed 里必须 return,且不要在 computed 里写异步逻辑。
搜索案例:watch 做行为,computed 做派生
搜索的需求是:输入关键词,列表只显示包含关键词的名字。为什么需要这个案例?它能说明 includes 的用法,也能对比两种方案。怎么想?数据有 arr、keyword,watch 方案再准备 resultArr:
js
watch: {
keyword(newVal) {
this.resultArr = this.arr.filter(item =>
item.toLowerCase().includes(newVal.toLowerCase())
)
}
}模板中 v-for="item in resultArr"。转写稿里强调:includes 可用于字符串,也可用于数组;但如果数组元素是对象,就不能直接判断对象是否包含字符串。易错点:空字符串会让 includes('') 为真,所以初态可能全部显示;需要时加 keyword && ... 控制。
用 computed 更直接:
js
computed: {
resultArr() {
return this.arr.filter(item =>
item.toLowerCase().includes(this.keyword.toLowerCase())
)
}
}为什么推荐这里用 computed?因为它就是“由 keyword 和 arr 算出结果”,有缓存,不需要额外维护 resultArr。watch 更适合关键词变化后还要发请求、打日志等“行为”。
全名拼接:多依赖优先 computed
为什么全名案例能体现差异?firstName 和 lastName 任一变化,全名都要变。用 watch 要监听两个字段,容易漏:
js
watch: {
firstName(val) { this.fullName = val + this.lastName },
lastName(val) { this.fullName = this.firstName + val }
}用 computed 只需一个返回值:
js
computed: {
fullName() {
return this.firstName + this.lastName
}
}怎么想?结果由多个数据计算而来,优先 computed。易错点:watch 只监听一个字段时,另一个字段变化不会触发,转写稿中“改了名右边没改”就是这个原因。
$watch 与 $el:实例上的侦听和挂载元素
vm.$watch 为什么需要?它可以在实例外部侦听数据。怎么做?
js
const vm = new Vue({ el: '#app', data: { name: '一' } })
vm.$watch('name', (newVal, oldVal) => {
console.log(newVal, oldVal)
})监听对象内部属性时要加深度监听:
js
vm.$watch('obj', callback, { deep: true })易错点:第三个参数才是配置对象,不是把 obj 写成对象就深度监听。
vm.$el 是 Vue 实例挂载的真实元素。为什么需要?有时要在实例外部操作挂载容器。怎么做?vm.$el.style.color = 'red'。易错点:$el 是真实 DOM,能改样式,但业务中不要频繁直接操作,优先用数据驱动。转写稿还提到 $data、$options,本节重点先掌握 $watch 与 $el 即可。
为什么需要 $watch
Vue 的核心是数据驱动视图:数据变了,模板自动更新。但有些行为不是“直接渲染”,比如数据变化后要操作 DOM、发请求、更新另一个数据。这时候就需要侦听数据的变化,在变化后执行一段逻辑。vm.$watch 就是 Vue 实例上提供的侦听方法,它不依赖模板,可以在实例外部调用,适合处理这类“数据变了要做事”的场景。
基本用法:监听基本类型
先看笔记中的代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="num++">{{num}}</button>
<input type="text" v-model="obj.userName">
</div>
<p></p>
</body>
<script>
const p =document.querySelector("p");
const vm = new Vue({
el:"#app",
data:{
num:1,
obj:{
userName:"laowangtou"
}
}
});
vm.$watch("num",function(newValue,oldValue){
p.innerText = newValue;
console.log("num",newValue,oldValue);
})
vm.$watch("obj",function(newValue,oldValue){
console.log("obj",newValue.userName)
},{
deep:true
})
</script>
</html>怎么想:把“要侦听的数据名”和“变化后要执行的回调”关联起来。$watch 的第一个参数是表达式字符串,第二个参数是回调函数,回调会收到新值和旧值。
怎么做:像代码里这样,vm.$watch("num", function(newValue, oldValue){ ... })。点击按钮让 num++,回调就会执行,把新值写进页面上的 <p>,同时打印新旧值。转写稿里也演示了监听 name,点击按钮后输出 newValue 和 oldValue,说明回调的两个参数分别代表变化后的值和变化前的值。
易错点:$watch 默认不会立即执行,只有数据真正变化后才触发;回调里如果要用 this 访问实例数据,建议用普通函数(Vue 会把 this 指向 vm),不要用箭头函数,否则 this 不是 vm。
深度侦听:监听对象内部属性
为什么需要:上面的 obj 是一个对象。直接改 obj.userName 时,obj 的引用并没有变,默认的 $watch("obj", ...) 不会触发。转写稿里调试时也发现:在输入框里改 obj.userName,回调没有执行。
怎么想:需要告诉 Vue“不仅要看对象本身,还要深入内部属性”。这就是深度侦听。
怎么做:在第三个参数里传 { deep: true }:
js
vm.$watch("obj", function(newValue, oldValue){
console.log("obj",newValue.userName)
},{
deep:true
})加了这个配置后,修改 obj.userName 就会触发回调。转写稿里还强调:第三个参数才是配置对象,不要写成第二个参数。
易错点:深度侦听会遍历对象内部属性,对象很大时有性能开销;另外,由于对象引用没变,回调里的 newValue 和 oldValue 往往指向同一个对象,不能靠它们直接对比内部属性的旧值。
立即执行与调试过程
转写稿里做了一个切换案例:用 index 控制显示哪一组新闻。先设置 infoArr 为空数组,然后 vm.$watch("index", function(newValue){ this.infoArr = this.newsList[newValue].infoList })。点击按钮时 index 变化,infoArr 跟着更新,页面内容切换。
但刷新页面后没有默认内容,因为 $watch 默认不立即执行。解决方法是第三个参数加 { immediate: true },让回调在侦听开始时先执行一次,把初始数据填进去。
调试时还遇到两个小问题:一是模板里绑定 class 要写 :class,写成 markclass 不会生效;二是 index 必须真正参与渲染,否则即使值变了,视图也不会响应。这些都属于模板写法和响应式使用中的常见坑。
与计算属性的对比
转写稿反复对比了侦听和计算属性:侦听适合“数据变化后执行行为”,不需要返回值,支持异步;计算属性适合“根据多个数据计算出一个结果”,必须有 return,有缓存,不支持异步。比如全名拼接,用计算属性写 return this.firstName + this.lastName 就很简单;用侦听则要同时监听姓和名,麻烦得多。所以不要用侦听去替代简单的派生数据,也不要用计算属性做副作用。
小结
$watch 的核心是:指定要侦听的数据,在变化后执行回调。基本类型直接监听;对象内部属性要加 deep: true;需要初始执行就加 immediate: true。结合笔记代码多调试几次,就能掌握它和计算属性的分工。
课堂画面





代码示例
示例 1:切换 —— 侦听(watch)实现
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#app button{
padding:5px;
margin:5px;
}
#app button.active{
background: red;
}
#app div{
width: 500px;
height: 300px;
border:1px solid green;
}
</style>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button :class="{active:index===i}" @click="index=i" v-for="(item,i) in newsList" :key="i">{{item.type}}</button>
<div>
<p v-for="item in infoArr" :key="item.id">{{item.newsTitle}}</p>
</div>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
index:1,
infoArr:[],
newsList:[
{
type:"体育",
infoList:[
{ id:11, newsTitle:"体育1" },
{ id:12, newsTitle:"体育2" }
]
},
{
type:"娱乐",
infoList:[
{ id:21, newsTitle:"娱乐1" },
{ id:22, newsTitle:"娱乐2" }
]
},
{
type:"财经",
infoList:[
{ id:31, newsTitle:"财经1" },
{ id:32, newsTitle:"财经2" }
]
}
]
},
watch:{
// index(){
// this.infoArr = this.newsList[this.index].infoList;
// }
index:{
handler(){
this.infoArr = this.newsList[this.index].infoList;
},
immediate:true
}
}
})
</script>
</html>要点
:class="{active:index===i}":动态 class 绑定对象写法,index是当前选中的下标,命中时按钮加.active变红。@click="index=i":点击把当前v-for的下标i赋给index,这是整个切换的“开关”。- 渲染区遍历的是
infoArr(初始为空数组),所以首屏不写immediate时页面是空白的。 handler里的this.infoArr = this.newsList[this.index].infoList是核心:index一变就把对应的infoList取出来。- 易错点:
watch默认不立即执行,刷新后首屏空白,必须写成对象形式加immediate:true;简写形式index(){}不支持immediate(笔记里注释掉的就是简写)。 - 易错点:
v-for一定要给:key,按钮用下标、内容用item.id,避免复用错乱。
示例 2:切换 —— 计算属性(computed)实现
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#app button{
padding:5px;
margin:5px;
}
#app button.active{
background: red;
}
#app div{
width: 500px;
height: 300px;
border:1px solid green;
}
</style>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button :class="{active:index===i}" @click="index=i" v-for="(item,i) in newsList" :key="i">{{item.type}}</button>
<div>
<p v-for="item in infoArr" :key="item.id">{{item.newsTitle}}</p>
</div>
</div>
</body>
<script>
// 侦听与计算属性的区别:
// 1- 侦听是通过watch声明,计算属性是通过computed声明
// 2- 侦听不需要返回值,计算属性要有返回值。
// 3- 侦听默认不会立即调用。计算属性在模板中调用才会执行
// 4- 侦听没有缓存,计算属性有缓存。
// 5- 侦听的应用:侦听某一数据发生改变后的一个行为。计算属性:受多个属性的变化而变化。
// 6- 侦听支持异步,计算属性不支持异步。
new Vue({
el:"#app",
data:{
index:1,
newsList:[
{
type:"体育",
infoList:[
{ id:11, newsTitle:"体育1" },
{ id:12, newsTitle:"体育2" }
]
},
{
type:"娱乐",
infoList:[
{ id:21, newsTitle:"娱乐1" },
{ id:22, newsTitle:"娱乐2" }
]
},
{
type:"财经",
infoList:[
{ id:31, newsTitle:"财经1" },
{ id:32, newsTitle:"财经2" }
]
}
]
},
computed:{
infoArr(){
return this.newsList[this.index].infoList;
}
}
})
</script>
</html>要点
- 把示例 1 里
data中的infoArr和整个watch删掉,改由computed中的infoArr()直接return,模板不用改一个字。 computed必须return,返回值就是模板要渲染的内容;不写return得到undefined,页面渲染不出东西。- 计算属性是“看结果”:它依赖
newsList和index,其中任一变化就重新计算,所以点击按钮列表自动跟着换。 - 易错点:
infoArr不要同时写在data里又写在computed里,会冲突;计算属性不能写异步(setTimeout里return拿不到值)。 - 易错点:计算属性有缓存,模板中多次使用也只执行一次;但若页面根本不使用它,它就永远不会执行——这和
watch默认不执行正好相反。
示例 3:全名拼接 —— 侦听 vs 计算属性对比
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="firstName" placeholder="请输入姓">+
<input type="text" v-model="lastName" placeholder="请输入名">
{{fullName}}|{{fullName2}}
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
firstName:"",
lastName:"",
fullName:""
},
watch:{
firstName(){
this.fullName = this.firstName+this.lastName;
},
lastName(){
this.fullName = this.firstName+this.lastName;
}
},
computed:{
fullName2(){
return this.firstName+this.lastName;
}
}
})
</script>
</html>要点
v-model分别绑定firstName/lastName,两边输入即时同步到数据。- 侦听方案:因为结果受两个数据影响,必须写两个
watch,firstName和lastName各一个,且里面是重复代码。 - 计算属性方案:
fullName2一行return this.firstName+this.lastName,Vue 自动收集依赖,谁变就重算,写法更简单。 - 易错点:侦听方案如果只写
firstName一个侦听器,改“名”的时候右边不更新——这是转写稿里强调的“这个案例两个区别就大”。 - 易错点:
fullName要先在data里声明成空字符串,否则第一次赋值不是响应式的,页面不刷新。
常见错误
1. 首屏空白,点了才有内容
- 现象:切换案例刷新后下面
<div>是空的,点一下按钮才出现新闻。 - 原因:
watch默认不立即执行,infoArr初值是[],只有index变化时才赋值。 - 修法:把
index的侦听写成对象形式并加immediate: true;或改用computed直接返回结果。
2. 在 watch 里 return 结果,页面没反应
- 现象:
watch: { keyword(){ return this.arr.filter(...) } },模板里v-for="item in keyword"什么都渲染不出来。 - 原因:
watch没有返回值语义,返回值被丢弃,模板拿到的还是 data 里的原值。 - 修法:在 data 里声明一个中转数组(如
resultArr),在 handler 里this.resultArr = ...;或改用computed。
3. computed 里写了异步/定时器,结果永远是旧值或 undefined
- 现象:
computed: { result(){ setTimeout(()=>{ return xxx }, 300) } },页面报错或渲染不出。 - 原因:计算属性必须同步
return,拿不到异步回调里的值;且它靠依赖收集缓存,异步更新不会触发重算。 - 修法:需要延迟、请求、防抖等异步行为一律交给
watch,在回调里改 data;computed只做同步派生。
4. 写了 computed 但页面没变化,以为没生效
- 现象:
computed里console.log不打印,或只打印一次,怀疑代码没跑。 - 原因:计算属性是懒执行的,只有模板(或其它计算属性/watch)真正读取它时才求值;且依赖不变时直接走缓存。
- 修法:确认模板中确实引用了它(
{{ infoArr }}或v-for="item in infoArr");调试时临时在模板里输出一次,或用watch观察。
5. $watch 监听对象内部属性不触发
- 现象:
vm.$watch('obj', cb)后修改obj.userName,回调不执行。 - 原因:默认只监听引用地址变化,改属性不改引用;另外把
{deep:true}误写成第二个参数(回调位)也会失效。 - 修法:
vm.$watch('obj', cb, { deep: true }),配置对象必须是第三个参数;或直接监听具体路径'obj.userName'。
6. 搜索框清空后列表整个消失 / 报错
- 现象:删掉关键字,页面变成空白,控制台报
Cannot read properties of undefined。 - 原因:
computed里if (this.keyword) return ...,关键字为空时函数返回undefined,v-for遍历undefined出错。 - 修法:无关键字时返回全量数组(
return this.keyword ? filter(...) : this.arr),或统一返回[]并配空态提示。
7. 用 index 当 key,切换数据源后内容错乱
- 现象:从"体育"切到"财经",偶尔出现旧标题残留、动画错位。
- 原因:
v-for用i作:key,不同数据源的下标重复,Vue 复用了旧节点。 - 修法:用数据自身的稳定标识,如
:key="item.id"。
练习
切换改造:把 9-2 的
watch版本改成computed版本,然后用console.log在计算属性里打点,分别在"刷新页面""点击同一个按钮两次""在页面上加一个无关的name按钮并点击"三种情况下记录打印次数,解释每次结果。搜索加防抖:把 9-4 的搜索改造成输入停止 300ms 后才过滤(
watch+setTimeout+ 清除上一个定时器)。再尝试用computed实现同样效果,说明为什么做不出来。全名合并:用
computed实现fullName,并额外提供"清空"按钮把firstName/lastName置空,验证fullName自动变空;再对比watch版本需要写几个 handler。$watch实践:在实例外部用vm.$watch监听obj的userName,分别用"监听路径'obj.userName'"和"监听'obj'+deep"两种写法,打印newValue/oldValue,观察深度监听时新旧值是否是同一个对象引用,并说明原因。$el操作:在#app外部放一个按钮,点击后通过vm.$el修改挂载容器内某个元素的样式;再对比document.querySelector('#app')拿到的对象是否一致。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 切换-侦听 | 12:37 |
| 2 | 搜索 | 27:58 |
| 3 | 全名-侦听-计算属性 | 06:19 |
| 4 | $watch | 07:47 |
| 5 | $el | 04:06 |
小结
watch面向"某个数据变了之后要做什么",是行为;computed面向"由哪些数据算出什么结果",是值。- 需要立即有初始结果、且结果是同步推导出来的,优先
computed;首屏要触发一次,用immediate: true。 computed必须有同步return,异步、定时器、请求一律放watch。computed懒执行且有缓存,不写进模板就不跑,依赖不变就不重算;watch无缓存,每次变化都执行。- 多个源决定一个结果(如姓 + 名、列表 + 关键字)用
computed更省事;watch版本往往要写多个 handler 或手动维护中转数据。 - 反过来,一个源触发一个副作用(发请求、改 DOM、写 localStorage)只能用
watch。 $watch是实例方法版侦听,第三个参数才是配置对象,监听对象内部变化必须deep: true。$el是挂载后的真实 DOM 根元素,与document.querySelector('#app')等价,可在实例外操作,但业务中应尽量用数据驱动而非直接改 DOM。