Skip to content

ref与v-for结合使用与ref与组件结合使用(Lesson5 3/3) ​

本页对应 Lesson05,共 6 段视频,约 59 分钟。

学习目标 ​

  • 能够使用 ref 获取表单输入值,并在 v-for 列表与组件场景中通过 ref 访问对应实例完成数据收集或方法调用。
  • 能够将微博案例拆分为列表、发布、分页等组件,并正确完成组件注册、嵌套与父子传参。
  • 能够调用接口获取微博列表,将数据渲染到页面,并在新增成功后通过事件总线或状态提升触发列表刷新。
  • 能够实现分页展示、页码高亮与页码切换,并在顶、踩、删除后刷新当前页数据。
  • 能够按接口文档完成添加、顶踩、删除请求,并根据响应结果进行成功提示、失败中断与列表更新。

知识点 ​

本节要解决的问题 ​

这一节只围绕一个核心:ref 在普通元素上和 v-for 列表上到底怎么取。笔记里的示例很短,但包含两个容易混淆的点:同名 ref 会覆盖,ref 与 v-for 结合会得到数组。转写稿里虽然大量在讲微博案例的组件拆分、添加、分页、顶踩和删除,但其中也用到了 ref 取输入框内容,并暴露了“取不到值要先检查 $refs 和 value”的调试过程。下面把这两条线合起来讲清楚。

普通元素上的 ref:为什么需要、怎么想、怎么做 ​

为什么需要:Vue 推荐数据驱动视图,但有些场景需要直接拿到 DOM,例如取输入框内容、读取元素文本、做聚焦等。ref 就是模板里给元素或组件起一个引用名,然后在实例里通过 this.$refs.名字 访问。

怎么想:先给元素写 ref="my",再在方法里打印 this.$refs.my。如果页面上有多个同名 ref,Vue 不会给你一个集合,而是后出现的覆盖前面的,所以最终拿到的是最后一个。

怎么做:看笔记中的两个 div:

html
<div ref="my">1</div>
<div ref="my">2</div>

在 fn 里如果执行 console.log(this.$refs.my),得到的是第二个 div,它的 innerText 是 2。所以笔记注释写“如果设置 ref 的值相同,那么后者会覆盖前者”。

易错点:不要以为同名 ref 会自动变成数组;只有 v-for 场景才会得到数组。取不到时先检查是否漏写 this.$refs、名字是否拼错、方法是否在 DOM 渲染后触发。

ref 与 v-for 结合:为什么得到数组、怎么取 ​

为什么需要:列表渲染时,每个 p 都是同一类元素,如果给它们同一个 ref,业务上往往需要拿到这一组元素,而不是最后一个。Vue 对 v-for 上的 ref 做了特殊处理,会收集成数组。

怎么想:模板里写 ref="myP" v-for="item in arr" :key="item",那么 this.$refs.myP 就是数组,数组顺序与渲染顺序一致。要取第一个元素,就写 this.$refs.myP[0]。

怎么做:笔记中的实际输出是:

js
console.log(this.$refs.myP[0].innerText)

因为 arr 是 [1,2,3,4],第一个 p 的文本是 1,所以控制台输出 1。

易错点:v-for 的 ref 不要当单个元素用;数组下标从 0 开始;key 要写稳定值,笔记里用 :key="item";列表更新后 $refs.myP 可能变化,不要在数据还没渲染时就去取。

完整示例:保留笔记代码 ​

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="fn">你过来呀!</button>
	<div ref="my">1</div>
	<div ref="my">2</div>
	<hr/>
	<p ref="myP" v-for="item in arr" :key="item">{{item}}</p>
</div>
</body>
<script>
	// 注意:如果设置ref的值相同,那么后者会覆盖前者。
	// 注意:如果ref与v-for结合使用,那么获取到的是数组。
	new Vue({
		el:"#app",
		data:{
			arr:[1,2,3,4]
		},
		methods:{
			fn(){
				// console.log(this.$refs.my)
				console.log(this.$refs.myP[0].innerText)
			}
		}
	})
</script>
</html>

用转写稿补一个真实调试过程:ref 取输入框 ​

转写稿里添加微博时,需要收集输入框内容。原话大意是“给 input 增加一个 ref,叫 contentRef,然后通过 this.$refs.contentRef.value 收集内容”。这里纠正同音字:REFREM 是 ref,扩到REO/contREF 是 contentRef,dollarREFS 是 $refs,Y 是 value。

为什么需要:输入框的值不在 data 里时,可以用 ref 直接读 DOM 的 value。

怎么想:先确认输入框已经渲染,再在点击事件里取 this.$refs.contentRef.value,然后判空。转写稿里判空写的是“长度等于 0 就提示请输入微博”。

怎么做:

js
const content = this.$refs.contentRef.value
if (content.length === 0) {
  alert('请输入微博')
  return
}

易错点:漏写 .value 会拿到元素本身;在 mounted 之前取可能拿不到;发送请求时如果接口要求 JSON,请求体要按接口文档传对象并设置对应类型,不要传错格式。转写稿还提到:删除或顶踩后要按“当前页”重新获取列表,否则容易回到第一页。这也是 ref 调试之外的常见联动问题。

小结:先打印,再取值 ​

遇到 ref 问题,最稳的调试顺序是:先 console.log(this.$refs) 看有哪些引用;再打印具体名字,看它是元素还是数组;最后取 innerText 或 value。记住两句话:同名普通 ref 后者覆盖前者;v-for 上的 ref 得到数组。

本节目标:ref 与组件结合使用 ​

为什么需要:父组件有时需要读取子组件内部状态,或者触发子组件里的数据变化。只靠 props 只能“父传子”,子组件自己的 data 父组件默认拿不到。ref 提供了一种直接拿到组件实例的方式。
怎么想:先给子组件在模板上打一个 ref 标记,父组件挂载完成后通过 this.$refs.标记名 找到这个子组件实例,再访问它的 data 或方法。
怎么做:笔记中的核心写法是 <child ref="suibian"></child>,父组件方法里执行 this.$refs.suibian.num++。
易错点:$refs 不是响应式的,不要在模板里依赖它做渲染;组件还没挂载时取不到;ref 名字不要和已有 data、方法重名,避免混淆。

ref 加在普通元素和组件上,拿到的东西不同 ​

为什么需要:同样叫 ref,加在 input 上和加在子组件上,结果不一样。不理解这一点,就容易把组件实例当 DOM 用。
怎么想:ref 是“引用标记”。加在普通标签上,$refs.xxx 得到 DOM 元素;加在组件标签上,得到组件实例。转写稿里的微博案例就用 ref 标记输入框,在方法里通过 this.$refs.xxx.value 收集输入内容,再判断是否为空、发请求。
怎么做:普通元素取 .value、.innerHTML 等;组件实例取 data 里的属性或组件方法。本节代码取的是子组件实例上的 num。
易错点:组件 ref 不能想当然地取 .value;在 v-for 里给组件加 ref,$refs 往往变成数组,要按数组处理;$refs 的值要在 mounted 之后访问才稳。

父组件操作子组件数据状态的推导 ​

为什么需要:笔记注释写得很明确:“ref 与组件结合使用,获取到的是该组件实例。通过该特点可以实现父级操作子组件中的数据状态。”
怎么想:子组件 Child 的 data 返回 { num: 1 }。父组件点击 app 按钮时,如果能拿到 Child 实例,就能访问实例上的 num。this.$refs.suibian 正是这个实例,所以 this.$refs.suibian.num++ 改的就是子组件的数据。子组件模板里又绑定了 {{num}},数据一变,视图跟着更新。
怎么做:父组件按钮绑定 fn,fn 中执行自增;子组件自己的按钮也绑定 num++。两个按钮操作的是同一个 num。
易错点:这是演示 ref 能力,不是鼓励到处直接改子组件 data。正式项目更推荐 props 向下、事件向上,或者用事件总线处理兄弟组件通信。转写稿里添加微博后要让列表更新,用的就是事件总线发布“get 微博”消息,而不是靠 ref 跨兄弟组件硬改。

完整代码保留 ​

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="fn">app</button>
	<hr/>
	<child ref="suibian"></child>
</div>
</body>
<script>
	// ref与组件结合使用,获取到的是该组件实例。
	// 通过该特点可以实现父级操作子组件中的数据状态。
	new Vue({
		el:"#app",
		methods:{
			fn(){
				// console.log(this.$refs.suibian.num);
				this.$refs.suibian.num++;
			}
		},
		components:{
			Child:{
				data(){
					return {
						num:1
					}
				},
				template:(`
					<button @click="num++">{{num}}</button>
				`)
			}
		}
		
	})
</script>
</html>

调试过程与转写稿补充 ​

为什么需要:看懂代码不够,要知道出错时怎么查。转写稿里的微博案例反复调试了“收集输入—判断空—发请求—更新列表”的过程,和本节 ref 机制是同一条线。
怎么想:先确认 $refs 里有没有目标。可以在 fn 里先 console.log(this.$refs.suibian),确认拿到的是组件实例而不是 DOM;再打印 this.$refs.suibian.num 看数据。转写稿中也先输出收集到的内容,再判断长度是否等于 0,为空就提示“请输入微博”。
怎么做:点击父按钮,观察子按钮数字是否变化;点击子按钮,观察父按钮是否还能继续改同一个数字。若报错“Cannot read property 'num' of undefined”,优先检查 ref 名字是否写错、组件是否已挂载。
易错点:不要一开始就在 created 里取组件 ref;不要在 ref 名字上随意改大小写;直接修改子组件状态后,如果发现视图没更新,先确认改的是不是响应式 data,而不是普通变量。转写稿里分页当前页可能大于总页数,服务端会纠正到最后一页;删除后重新获取当前页也可能被服务端纠正。这类“状态要以接口返回为准”的思路,也适用于理解组件状态不要乱猜。

先启动环境:数据库、API 与依赖 ​

这个案例“麻雀虽小,五脏俱全”,增删改查、组件拆分、兄弟组件通信、分页都能练到。为什么先启动环境?因为页面上的添加、渲染、删除、顶踩、分页都要调接口,接口背后还要读写 MongoDB;环境没起来,后面所有功能都只能“看代码猜结果”。

怎么想:按依赖顺序启动——先数据库,再 API,最后页面。怎么做:本节笔记给出的启动步骤如下,先原样保留:

bash
# 启动数据库
mongod --dbpath e:\mongo

# 进入 api 目录安装依赖
cnpm i

# 启动 api
node server.js

注意 --dbpath 后面的目录必须真实存在;转写稿里反复强调“这个地址得有”。如果本机把 MongoDB 服务禁用了,就手动执行 mongod,看到光标持续闪烁说明挂载成功;再开一个终端用 mongo 进入数据库环境。API 目录安装依赖时用 cnpm i(npm/yarn 也可,以本机为准),启动 server.js 后接口端口按转写稿是 8090,并已处理跨域。易错点:不要在 API 目录里随便删依赖或重复下载整套资料;转写稿明确提醒“有 node_modules 就不要乱下这个 API”。启动后先按接口文档确认添加、获取、删除、修改的参数和返回格式,再写前端。

拆分组件:先让结构跑起来 ​

为什么拆?上方“发微博/添加”和下方“列表”是兄弟关系,分页又适合独立成组件;不拆的话,添加成功后很难通知列表更新。怎么想:先按页面区域拆成“添加组件”“列表组件”“分页组件”,再处理通信。怎么做:在组件目录新建添加组件和列表组件,把原页面对应 DOM 剪切进去;模板较长时把 template 单独提出来,组件里 export default { template, data, methods... }。父组件引入并注册:

js
import TakeComment from './components/TakeComment.vue'
import CommentList from './components/CommentList.vue'

export default {
  components: { TakeComment, CommentList }
}

易错点:组件名、引入路径、注册名要一致;剪切 DOM 后别忘了把依赖的样式和图片一起复制过来,否则页面“啥也看不到”。先保证拆分后页面能显示,再写功能。

添加微博:ref 收集、接口调用与失败中断 ​

为什么用 ref?输入框内容需要被按钮事件读取。怎么想:按钮点击 → 取输入值 → 校验空值 → 按接口文档发请求 → 判断成功/失败 → 成功后清空输入并通知列表刷新。怎么做:给输入框加 ref="contentRef",按钮加 @click="addWeibo";方法里取 this.$refs.contentRef.value,为空提示“请输入微博”。请求按文档发 POST,请求体为 JSON;响应里用 ok===1 判断成功,失败用 msg 提示。转写稿还提到失败时用 new Promise(() => {}) 让后续代码不再执行,这是“中断流程”的做法。易错点:XHR 不要直接把对象塞进 send,要按封装/文档序列化并设置 Content-Type: application/json;成功判断不要只看 HTTP 状态,还要看业务字段 ok。数据库可用 db.集合名.find() 验证是否真的写入。

渲染与兄弟通信:事件总线 ​

为什么添加成功后下面不更新?因为列表组件没有重新获取数据。怎么想:列表在 mounted 获取一次;添加、顶踩、删除成功后都要再获取。兄弟组件通信用事件总线:在入口给 Vue.prototype.$bus = new Vue();列表组件 mounted 里 this.$bus.$on('get微博', this.getWeibo);添加组件成功后 this.$bus.$emit('get微博')。怎么做:列表数据放 contentList: [],接口返回后赋值 this.contentList = res.data.contentList;用 v-for 渲染,v-if="contentList.length===0" 显示“暂无留言”。易错点:订阅名和发布名必须一致;列表组件销毁时最好 $off,避免重复订阅。

分页:props、高亮与页码来源 ​

为什么分页不能前端自己算当前页?转写稿调试发现:你传 20 页,但后端总共只有 3 页,后端会把页码纠正到最后一页。怎么想:总页数和当前页应以接口返回为准;分页组件通过 props 接收 pageCount、pageNum。怎么做:页码从 1 开始 v-for,高亮写 :class="{ active: i === pageNum }";点击页码发布事件 this.$bus.$emit('get微博', i),列表组件接收页码调用获取接口。易错点:顶踩/删除后重新获取时要传“当前页”,不要默认第一页,否则用户在第 3 页操作后会被拉回第 1 页;删除后当前页可能变空,仍以服务端返回为准。

顶踩与删除:改完仍留在当前页 ​

顶踩按文档是修改接口,传唯一标识 _id 和类型(1 顶、2 踩),成功后重新获取当前页。删除按文档把 id 拼在地址里,请求方式为 DELETE;点击删除传 item._id,删除成功后仍获取当前页。转写稿调试点很关键:顶踩后回到第一页,是因为刷新时没把当前页码传进去;删除报错则要检查请求方法/封装是否写错。易错点:_id 是唯一标识,别写成 id;删除和顶踩都要“先调接口,成功后再刷新列表”,失败则按接口返回提示处理。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:ref 与 v-for 结合使用 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>ref与v-for结合使用</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<button @click="fn">你过来呀!</button>
	<div ref="my">1</div>
	<div ref="my">2</div>
	<hr/>
	<p ref="myP" v-for="item in arr" :key="item">{{item}}</p>
</div>
</body>
<script>
	// 注意:如果设置ref的值相同,那么后者会覆盖前者。
	// 注意:如果ref与v-for结合使用,那么获取到的是数组。
	new Vue({
		el: "#app",
		data: {
			arr: [1, 2, 3, 4]
		},
		methods: {
			fn() {
				// console.log(this.$refs.my)        // 只拿到第 2 个 div
				// console.log(this.$refs.myP)        // 拿到的是一个数组
				console.log(this.$refs.myP[0].innerText)
			}
		}
	})
</script>
</html>

要点解释

  • 两个 div 的 ref 都写成 my,this.$refs.my 只会拿到最后一个,前面的被覆盖 —— 这是最容易踩的坑,同名 ref 不要用在并列元素上。
  • ref 写在 v-for 的元素上,this.$refs.myP 得到的是数组,顺序和渲染顺序一致,用下标 [0] 取第一个。
  • $refs 不是响应式的,只在渲染完成后才有值;在 created 里访问是 undefined,至少放到 mounted 或点击事件里。
  • :key 必须绑定,否则列表更新时 DOM 复用,$refs 数组顺序可能和 arr 对不上。

示例二:ref 与组件结合使用 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>ref与组件结合使用</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<button @click="fn">app</button>
	<hr/>
	<child ref="suibian"></child>
</div>
</body>
<script>
	// ref与组件结合使用,获取到的是该组件实例。
	// 通过该特点可以实现父级操作子组件中的数据状态。
	new Vue({
		el: "#app",
		methods: {
			fn() {
				// console.log(this.$refs.suibian.num);
				this.$refs.suibian.num++;   // 父组件直接改子组件的数据
			}
		},
		components: {
			Child: {
				data() {
					return {
						num: 1
					}
				},
				template: `
					<button @click="num++">{{num}}</button>
				`
			}
		}
	})
</script>
</html>

要点解释

  • ref 写在自定义组件标签上,this.$refs.suibian 拿到的是组件实例,不是 DOM 元素;可以直接 .num 访问子组件的 data、.fn() 调子组件的方法。
  • 父点 app 按钮和子组件自己点按钮,改的是同一份 num,因为拿到的就是同一个实例。
  • 易错点:ref 写在组件上就不能再用 innerText、style 这类 DOM 属性了,要操作 DOM 得写 this.$refs.suibian.$el。
  • 这只是"能改",属于父子通信的兜底手段;正式项目更推荐 props / $emit。本节转写稿里"添加完成后让下面的列表刷新"用的就是另一种方案 —— 事件总线(见示例三)。

示例三:ref 收集输入 + 事件总线让兄弟组件刷新(转写稿微博案例简化版) ​

转写稿中列表数据来自 axios 请求接口(get微博 / post微博),这里用本地数组代替接口,其余结构(ref 取值、校验、$bus 通知)与课上完全一致。

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>微博案例:ref + 事件总线</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<weibo-input></weibo-input>
	<hr/>
	<weibo-list></weibo-list>
</div>
</body>
<script>
	// 模拟数据库里的数据(真实项目里是 axios 请求接口拿回来的)
	let db = [
		{ _id: 1, content: '第一条微博' },
		{ _id: 2, content: '第二条微博' }
	];

	// 事件总线:兄弟组件通信
	Vue.prototype.$bus = new Vue();

	// 上面的组件:输入 + 提交
	Vue.component('weiboInput', {
		template: `
			<div>
				<input ref="contentRef" placeholder="请输入微博内容">
				<button @click="postWeibo">提交评论</button>
			</div>
		`,
		methods: {
			postWeibo() {
				// 用 ref 拿到输入框的值(转写稿:this.$refs.contentRef.value)
				let content = this.$refs.contentRef.value;
				if (content.length < 1) {
					alert('请输入微博内容');
					return;   // 校验不通过直接中断
				}
				db.push({ _id: Date.now(), content: content });
				// 清空输入框
				this.$refs.contentRef.value = '';
				// 发布消息,通知兄弟组件重新获取数据
				this.$bus.$emit('get微博');
			}
		}
	});

	// 下面的组件:列表
	Vue.component('weiboList', {
		template: `
			<div>
				<p v-if="list.length === 0">暂无留言</p>
				<ul>
					<li v-for="item in list" :key="item._id">{{ item.content }}</li>
				</ul>
			</div>
		`,
		data() {
			return { list: [] }
		},
		mounted() {
			// 订阅:只要有人发 'get微博',就重新取一次数据
			this.$bus.$on('get微博', this.getWeibo);
			// 打开页面先取一次
			this.getWeibo();
		},
		beforeDestroy() {
			// 一定要解绑,否则组件销毁后订阅还在,会重复触发
			this.$bus.$off('get微博', this.getWeibo);
		},
		methods: {
			getWeibo() {
				this.list = db.slice();   // 真实项目:this.$axios.get(url).then(...)
			}
		}
	});

	new Vue({ el: '#app' });
</script>
</html>

要点解释

  • ref 拿到的输入框值要在点击时取,不能在 data 里写死;取值用 this.$refs.contentRef.value,少写 .value 就是拿到整个 DOM 元素。
  • 校验用 content.length < 1,return 必须写,转写稿里就是用这个思路挡住"空内容提交"。
  • 兄弟组件(输入组件、列表组件)之间没有父子关系,用 Vue.prototype.$bus = new Vue() 造一个全局事件总线;发布 $emit、订阅 $on,两个名字必须一字不差(课上叫 get微博)。
  • 易错点一:$on 写在 mounted 里,且 beforeDestroy 中要 $off,否则切换页面后消息被处理多次。
  • 易错点二:转写稿里顶/踩、删除之后都要带上当前页码重新 getWeibo(pageNum),否则默认回第 1 页;这里同理,$emit 之后列表重新取数即可。

常见错误 ​

  1. 两个普通元素写同一个 ref="my",只拿到最后一个

    • 现象:this.$refs.my 是单个 DOM,this.$refs.my.innerText 永远是 "2",第一个 div 拿不到。
    • 原因:非 v-for 场景下 ref 是"键值对"语义,同名键后者覆盖前者,不会自动变数组。
    • 修法:改成唯一名字(ref="a" / ref="b");确实要批量取就套上 v-for(此时才是数组),或用 this.$el.querySelectorAll。
  2. v-for 里的 ref 数组拿到 null 或长度对不上

    • 现象:this.$refs.myP[2].innerText 报 Cannot read property 'innerText' of undefined。
    • 原因:ref 数组按渲染顺序收集,循环项里有 v-if 未通过时该项不渲染,数组长度小于 arr.length;或在 mounted 之前访问。
    • 修法:用 this.$refs.myP.filter(Boolean) 过滤;访问时机放到 mounted / this.$nextTick 之后。
  3. axios 传对象与传字符串,后端收到的内容不一样

    • 现象:post(url, { content }) 后端能拿到;改成 post(url, content) 后后端 req.body.content 为 undefined。
    • 原因:第二个参数是对象时默认 Content-Type: application/json;是字符串时退化为 application/x-www-form-urlencoded,字段名丢了,服务端 body-parser 解析路径不同。
    • 修法:统一传对象并在拦截器里固定 header,或显式 headers: {'Content-Type': 'application/json'} 且传 JSON.stringify(...)。
  4. 加了响应拦截器 return res.data 后,业务里仍写 res.data.ok

    • 现象:TypeError: Cannot read properties of undefined (reading 'ok'),成功失败判断永远走不进去。
    • 原因:拦截器已经把响应体剥了一层,then 里收到的就是业务数据本身。
    • 修法:全项目统一约定返回层级——要么拦截器只 return response,要么业务里直接 res.ok;拦截器内判断失败时 return Promise.reject(...) 中断后续流程,别让失败的代码继续往下跑。
  5. 兄弟组件:添加成功后列表不刷新

    • 现象:接口返回成功、数据库已有数据,但下面的列表还是旧的,必须手动 F5。
    • 原因:添加在 A 组件、列表在 B 组件,A 改了数据 B 无从得知,mounted 只跑一次。
    • 修法:事件总线($emit/$on 同名事件)或状态提升到公共父级;$on 必须在 beforeDestroy 里 $off 同名事件,否则来回切组件后一次添加会触发多次请求。
  6. 删除 / 顶踩后页码跳回第一页

    • 现象:在第 4 页点"顶",列表刷成了第 1 页的数据,但页码高亮还在 4。
    • 原因:刷新函数形参没传,pageNum = pageNum || 1 兜底成了 1。
    • 修法:刷新时显式传当前页 this.get微博(this.pageNum);删除后再判断"当前页 > 总页数"则把页码减一,同时把页码回传给分页组件,别让服务端纠正的页码和前端状态脱节。

练习 ​

  1. 给微博案例的输入框加一个 ref,页面 mounted 后自动聚焦;再写一个"清空"按钮,清空后重新聚焦。要求说明为什么用 ref 而不是 querySelector。
  2. 分页组件目前把 pageNum 当普通数据用。请改成"父传 props + 子 $emit('change', n)"的单向数据流,并说明改造后"点击页码高亮不更新"的问题为什么消失。
  3. 在 comment 组件里删除第 4 页最后一条数据,观察请求的页码与返回内容。写出你的页码修正逻辑(含"删空后显示暂无留言"的边界)。
  4. 给事件总线补上完整生命周期:列出 $on 注册与 $off 解绑分别写在哪两个钩子、为什么。写一个能复现"添加一次触发三次请求"的步骤。
  5. 用 ref + v-for 实现:点击任意一条微博内容,把它 content 变成可编辑的 input(同一时刻只允许一条处于编辑态)。要求只用一个 ref 名,并说明如何正确取到当前被点击那一条的 DOM。

原视频对照 ​

#视频时长
1组件拆分14:49
2获取数据14:56
3完成添加与渲染11:49
4分页11:11
5顶与踩04:14
6删除02:49

小结 ​

  • ref 在普通元素上是"单个",在 v-for 上才是"数组",同名覆盖是设计行为,不是 bug。
  • ref 拿到的是真实 DOM / 组件实例,属于"逃生舱":能不用就不用,能绑事件、能绑 v-model 的地方优先用数据驱动。
  • 跨组件通信先想清楚关系:父子走 props/$emit,兄弟走事件总线或状态提升;总线一定要配 $off,否则是内存泄漏 + 重复请求双杀。
  • axios 实例按"服务器"维度创建,baseURL 集中管理;Content-Type 与传参类型强相关,接口文档怎么写就怎么传。
  • 响应拦截器是"统一出口",剥层级和失败中断都放这里,业务代码里不要再做二次判断。
  • 所有"改数据后要刷新"的操作,都必须把当前页码一起带上,否则默认值会把用户送回第一页。
  • 分页是前端状态与服务端状态的对账:前端页码可能不准(超过总页数会被服务端纠正),所以页码以服务端返回的为准回写。
  • 本地跑不起来先别改代码:mongod --dbpath → cnpm i → node server.js,三步顺序错一步就是 Network Error。