Skip to content

切换-侦听与切换-计算属性(Lesson6 2/3) ​

本页对应 Lesson06,共 5 段视频,约 58 分钟。

学习目标 ​

  1. 能使用 watch 监听 index 等数据变化,并同步更新派生数组,完成选项卡切换内容渲染。
  2. 能使用 computed 实现相同的切换、搜索与全名拼接功能,并说明其与 watch 在返回值、缓存、初始调用和异步支持上的区别。
  3. 能独立实现关键字搜索过滤,使用 includes 完成大小写兼容的数组筛选,并分别用 watch 与 computed 完成同一需求。
  4. 能实现 firstName 与 lastName 的全名拼接,并根据场景选择 watch 或 computed 方案。
  5. 能使用 vm.$watch 在实例外部监听数据变化,正确传入监听字段、回调函数及 deep 配置完成深度侦听。
  6. 能使用 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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例 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"。

练习 ​

  1. 切换改造:把 9-2 的 watch 版本改成 computed 版本,然后用 console.log 在计算属性里打点,分别在"刷新页面""点击同一个按钮两次""在页面上加一个无关的 name 按钮并点击"三种情况下记录打印次数,解释每次结果。

  2. 搜索加防抖:把 9-4 的搜索改造成输入停止 300ms 后才过滤(watch + setTimeout + 清除上一个定时器)。再尝试用 computed 实现同样效果,说明为什么做不出来。

  3. 全名合并:用 computed 实现 fullName,并额外提供"清空"按钮把 firstName/lastName 置空,验证 fullName 自动变空;再对比 watch 版本需要写几个 handler。

  4. $watch 实践:在实例外部用 vm.$watch 监听 obj 的 userName,分别用"监听路径 'obj.userName'"和"监听 'obj' + deep"两种写法,打印 newValue/oldValue,观察深度监听时新旧值是否是同一个对象引用,并说明原因。

  5. $el 操作:在 #app 外部放一个按钮,点击后通过 vm.$el 修改挂载容器内某个元素的样式;再对比 document.querySelector('#app') 拿到的对象是否一致。

原视频对照 ​

#视频时长
1切换-侦听12:37
2搜索27:58
3全名-侦听-计算属性06:19
4$watch07:47
5$el04:06

小结 ​

  1. watch 面向"某个数据变了之后要做什么",是行为;computed 面向"由哪些数据算出什么结果",是值。
  2. 需要立即有初始结果、且结果是同步推导出来的,优先 computed;首屏要触发一次,用 immediate: true。
  3. computed 必须有同步 return,异步、定时器、请求一律放 watch。
  4. computed 懒执行且有缓存,不写进模板就不跑,依赖不变就不重算;watch 无缓存,每次变化都执行。
  5. 多个源决定一个结果(如姓 + 名、列表 + 关键字)用 computed 更省事;watch 版本往往要写多个 handler 或手动维护中转数据。
  6. 反过来,一个源触发一个副作用(发请求、改 DOM、写 localStorage)只能用 watch。
  7. $watch 是实例方法版侦听,第三个参数才是配置对象,监听对象内部变化必须 deep: true。
  8. $el 是挂载后的真实 DOM 根元素,与 document.querySelector('#app') 等价,可在实例外操作,但业务中应尽量用数据驱动而非直接改 DOM。