Skip to content

动态组件与ref(Lesson5 2/3) ​

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

学习目标 ​

  • 能准确说明局部组件与全局组件的作用范围差异,并能判断某组件在指定实例或组件中是否可用。
  • 能使用 Vue.component 注册全局组件,并能按模块化方式抽离全局组件配置、集中导入后完成全局注册。
  • 能使用 <component :is="..."> 实现动态组件切换,并说明其与 v-if/v-show 控制显示隐藏的本质区别。
  • 能通过 ref 获取普通 DOM 元素或表单元素,并读取、修改其内容或属性。
  • 能说明 ref 与 v-for 结合使用时返回数组而非单个元素,并能获取指定索引对应的元素。
  • 能通过组件上的 ref 获取子组件实例,并在父组件中访问其数据或调用其方法。

知识点 ​

需求与不用动态组件的写法 ​

为什么需要:页面上有 one、two、three、four 四个组件,希望点“切换”按钮时依次只显示其中一个。如果不用动态组件,最直接的想法是写多个 v-if / v-else-if,用 index 控制哪一个出现。

怎么想:先把四个组件都注册好,再在模板里写:

html
<one v-if="index===0"></one>
<two v-else-if="index===1"></two>
<three v-else-if="index===2"></three>
<four v-else-if="index===3"></four>

按钮点击时让 index++,超过 3 就回到 0。转写稿里这段调试过程就是:先写死 v-if 判断,点按钮改 index,能切换,但组件一多就要写很多分支,模板会越来越长。

怎么做:这种写法能实现效果,但本质是“手写分支判断”,每增加一个组件就要增加一条判断。

易错点:index 的边界要处理,this.index++ 后要判断 if(this.index>3) this.index=0;否则加到 4 就没有组件匹配。

动态组件的基本用法 ​

为什么需要:多个组件切换时,不希望写一堆 v-if / v-else-if,而是让一个“占位组件”根据变量自动变成目标组件。

怎么想:Vue 提供内置组件 component,它不是我们自己注册的组件,而是 Vue 给的。它靠 is 属性决定当前渲染哪个组件。转写稿里把 is 听成“意思”,这里统一纠正为 is。is 的值是组件名字符串,也可以绑定变量。

怎么做:先写:

html
<component is="one"></component>

再把 is 改成动态绑定:

html
<component :is="arr[index]"></component>

其中 arr:["one","two","three","four"],index 从 0 开始。点击按钮时 index 改变,arr[index] 取到不同组件名,component 就渲染对应组件。转写稿里验证过:is="one" 显示 one,改成 is="two" 就显示 two。

易错点:is 的值要写组件名,不是组件对象;组件名要和 components 里注册的名字对应。使用 :is 时,值可以是表达式,例如 arr[index]。

用数组和索引驱动切换 ​

为什么需要:如果每次切换都手动改 is 的值,仍然不够灵活;把组件名放进数组,用索引取值,按钮只负责改索引,逻辑更统一。

怎么想:数据里维护 arr 和 index,视图里只写一个动态组件。按钮方法只做 index++ 和边界归零。这样“组件列表”和“当前显示谁”分开,增加组件只需改数组和注册。

怎么做:本节示例的核心就是:

html
<component :is="arr[index]"></component>
js
changeIndex(){
  this.index++;
  if(this.index>3) this.index=0;
}

易错点:数组里写的是组件名字符串,不是 One 这个对象;大小写要按注册名匹配。示例中注册的是 One、Two,数组写 "one"、"two",在 Vue 1/早期写法里按名字匹配,实际项目要保证名字一致。

循环写法的辨析与易错点 ​

为什么需要:转写稿里还试过 v-for + v-show:

html
<component v-show="index===i" v-for="(item,i) in arr" :is="item" :key="i"></component>

它能把四个组件都创建出来,再根据 index 显示其中一个。

怎么想:这种写法虽然“看起来也能切换”,但本质是显示与隐藏,不是让一个动态组件真正变成另一个组件。转写稿明确说:下面这种 v-show 写法只是实现显示隐藏,动态组件的 is 才是决定“当前 component 代表谁”。

怎么做:如果只是展示切换,推荐用 :is="arr[index]" 的单个动态组件写法;如果确实要保留多个实例再控制显示,才考虑 v-for + v-show,并且必须加 :key,否则会有警告。

易错点:v-for 要写 :key;不要把“显示隐藏”和“动态组件切换”混为一谈;v-show 不会销毁/重建组件,和 :is 切换的语义不同。

本节完整代码如下:

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="changeIndex">切换</button>
	<!--使用动态组件:component是内置组件,is属性的值是要使用的组件的名字-->
	<component :is="arr[index]"></component>
<!--	<component v-show="index===i" v-for="(item,i) in arr" :is="item" :key="i"></component>-->
	
	<!--未使用动态组件-->
<!--	<one v-if="index===0"></one>-->
<!--	<two v-else-if="index===1"></two>-->
<!--	<three v-else-if="index===2"></three>-->
<!--	<four v-else-if="index===3"></four>-->
</div>
</body>
<script>
	new Vue({
		el:"#app",
		data:{
			arr:["one","two","three","four"],
			index:0
		},
		methods:{
			changeIndex(){
				this.index++;
				if(this.index>3) this.index=0;
			}
		},
		components:{
			One:{
				template:(`
					<div style="background:red">one</div>
				`)
			},
			Two:{
				template:(`
					<div style="background:green">two</div>
				`)
			},
			Three:{
				template:(`
					<div style="background:yellow">three</div>
				`)
			},
			Four:{
				template:(`
					<div style="background:pink">four</div>
				`)
			}
		}
	})
</script>
</html>

从一句笔记说起:ref 是“模板标记 + 实例引用” ​

本节笔记只有一句结论:可以通过 ref 获取真实 DOM。转写稿进一步把它拆成了三种典型场景:ref 与普通元素、ref 与 v-for、ref 与组件结合使用。先建立整体思路:ref 不是数据绑定,也不是替代 v-model 的通用方案;它是在模板上打一个名字,Vue 在渲染后把对应的真实 DOM 或组件实例放进 this.$refs,需要时再取。

为什么需要:Vue 强调数据驱动,但有些操作天然要碰 DOM,例如只收集输入值、读取元素内容、改原生样式,或者父组件要调用子组件里的数据/方法。
怎么想:先问“我拿的是 DOM 还是组件实例?”普通标签上写 ref,拿到 DOM;组件标签上写 ref,拿到组件实例;v-for 上写 ref,拿到数组。
怎么做:模板写 ref="名字",JS 里用 this.$refs.名字。
易错点:$refs 是普通对象,不是响应式数据;没设置 ref 时它可能是空对象;同名 ref 要特别小心。

ref 与普通元素结合:拿到 DOM 后再操作 ​

为什么需要:转写稿举了“输入内容后点击提交”的例子。这里只想收集内容,不需要视图和数据双向同步,用 v-model 反而多一层;用 ref 直接取输入框更直接。
怎么想:给按钮、div、input 加 ref,事件里通过 this.$refs.xxx 访问。
怎么做:

html
<button ref="btn" @click="fn">按钮</button>
<input ref="id" />
js
methods: {
  fn() {
    console.log(this.$refs.btn.innerText)
    console.log(this.$refs.id.value)
  }
}

转写稿还演示了:给 div 设置 ref="box" 后,可以用 this.$refs.box.style.color = 'pink' 改原生样式;这和 document.querySelector 能达到类似效果。
易错点:第一,ref 名字要和 $refs 后面的属性一致;第二,转写稿调试时把 ref 属性去掉,再点击输出,$refs 就是空对象,说明“没设置就没有”;第三,普通元素上若两个标签用了同一个 ref 名,后者会覆盖前者,转写稿明确说“后者会把前者给覆盖掉”。

ref 与 v-for 结合:得到的是数组 ​

为什么需要:列表渲染后往往要拿多个元素,例如读取每个 p 的内容。
怎么想:普通元素 ref 是一个 DOM;v-for 上的 ref 会收集成数组,数组元素就是循环出来的 DOM。
怎么做:

html
<p v-for="item in arr" :key="item" ref="myP">{{ item }}</p>
js
data() {
  return { arr: [1, 2, 3, 4] }
},
methods: {
  fn() {
    console.log(this.$refs.myP)        // 4 个 p 元素
    console.log(this.$refs.myP[0].innerText) // 1
  }
}

转写稿调试时输出 this.$refs.myP,看到的是 4 个 P 元素,并强调“v-for 出来几个,数组里就有几个”。
易错点:不要把它当成单个 DOM;不要以为只拿最后一个。转写稿还提到面试区别:React 里 ref 与循环结合通常拿到最后一个,而 Vue 这里得到数组,这是可说的差异点。另外 :key 要写,避免列表更新问题。

ref 与组件结合:拿到的是组件实例 ​

为什么需要:父组件有时要读子组件数据或调用子组件方法。转写稿的例子是子组件里有 name,父组件想通过按钮直接改子组件的 name。
怎么想:ref 写在组件标签上,拿到的不是 DOM,而是该组件的 Vue 实例;实例上能访问 data 里的属性和方法。
怎么做:

html
<card ref="sui"></card>
<button @click="fn">父按钮</button>
js
methods: {
  fn() {
    console.log(this.$refs.sui.name)
    this.$refs.sui.name++
  }
}

转写稿调试时先输出 this.$refs,没设置组件 ref 时没有目标;给组件加上 ref="随便" 后,this.$refs.随便 就是组件实例,能读到 name,也能在父级点击时修改子组件状态。
易错点:组件 ref 不是真实 DOM,不能直接当元素改样式;不要在组件还没渲染时读取;父组件直接操作子组件会形成耦合,能通过 props/events 解决时优先用它们。转写稿的结论是:ref 与组件结合可用于“父级操作子组件中的数据状态”,但要知道这是直接访问,不是常规通信首选。

调试结论:三种场景别混用 ​

把转写稿的调试过程收束一下:普通元素 ref 取到单个 DOM;去掉 ref 后 $refs 为空对象;同名普通 ref 后者覆盖前者;v-for 上 ref 得到数组;组件上 ref 得到组件实例。为什么需要:这些差异决定了你后面写 $refs.xxx.value、$refs.xxx[0] 还是 $refs.xxx.name。怎么想:先看 ref 写在谁身上。怎么做:按“普通元素→单个、v-for→数组、组件→实例”取用。易错点:不要把所有 ref 都当 DOM;不要用 ref 替代数据流;读取时机放在事件或挂载完成后。

为什么需要 ref 与普通元素结合 ​

在 Vue 里,表单输入常用 v-model 做双向绑定:数据变,视图变;视图变,数据也变。但有些场景并不需要双向绑定,比如输入完内容后点击“提交”,只是想把输入框里的值收集一次;或者想直接操作某个真实 DOM 的样式。这时再用 v-model 就有点“重”,用 ref 更直接。

ref 与普通元素结合使用,核心作用是:给元素加一个标记,Vue 挂载后可以通过 this.$refs 拿到对应的真实 DOM。它比 document.querySelector 更贴近 Vue 的管理范围,也不容易因为页面里有多个同类元素而选错。转写稿里也强调,这种用法在实际开发中“用得比较多”。

核心思路:先设置,再获取 ​

可以把它记成两步:

  1. 设置:为普通元素增加 ref 属性,值是字符串,例如 ref="btn"。
  2. 获取:在 Vue 实例方法里通过 this.$refs.btn 获取真实 DOM。

注意这里的 ref 值不是变量,而是字符串键名。你写 ref="btn",获取时就是 this.$refs.btn;你写 ref="my",获取时就是 this.$refs.my。this.$refs 本身是一个对象,里面存放着当前实例中所有已设置的 ref。

完整示例与调试过程 ​

下面保留本节笔记代码。代码里同时对比了事件对象、原生 document.querySelector 和 this.$refs 三种获取按钮的方式,并演示了修改 div 样式、读取 input 值。

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" ref="inp">
	<div ref="my">my</div>
	<button ref="btn" @click="fn">点我</button>
</div>
</body>
<script>
	// 1- 设置:为元素增加ref属性,值为字符串
	// 2- 获取:通过this.$refs.字符串-获取真实DOM
	new Vue({
		el:"#app",
		methods:{
			fn(e){
				console.log(e.target.innerText);
				console.log(document.querySelector("button").innerText);
				console.log(this.$refs.btn.innerText);
				console.log(this.$refs.btn === document.querySelector("button"));// true
				console.log(this.$refs.my);
				this.$refs.my.style.color = "pink";
				console.log(this.$refs.inp.value)
			}
		}
	})
</script>
</html>

调试时点击按钮,e.target.innerText 能拿到当前点击按钮的文字;document.querySelector("button").innerText 也能拿到按钮文字;this.$refs.btn.innerText 同样能拿到。关键一行是:

js
console.log(this.$refs.btn === document.querySelector("button")); // true

结果为 true,说明 this.$refs.btn 拿到的就是真实 DOM,而不是 Vue 包装出来的虚拟对象。接着 this.$refs.my 能拿到那个 div,所以可以继续写 this.$refs.my.style.color = "pink",用原生 DOM 方式改颜色。输入框设置 ref="inp" 后,this.$refs.inp.value 就能读取用户输入的内容。

转写稿里还验证过一个现象:如果把某个元素的 ref 属性去掉,再点击输出 this.$refs,会看到它是一个空对象。也就是说,没有设置 ref 的元素不会出现在 this.$refs 里。

易错点与使用边界 ​

第一,ref 的值是字符串,获取时不要漏掉 $refs,也不要写成 this.refs。第二,this.$refs 是对象,里面按你设置的字符串名存放 DOM;名字写错就取不到。第三,普通元素如果重复设置相同的 ref 值,转写稿后续验证过:后者会覆盖前者,所以普通元素一般应使用不同的 ref 名。第四,ref 拿到的是真实 DOM,可以直接改 style、读 value,但它不是响应式数据,不要把它当 data 里的状态来用。第五,本节只讲普通元素;转写稿还提到 ref 与 v-for 结合会得到数组、与组件结合会得到组件实例,这些是后续边界,先不要把普通元素的“单个 DOM”结论套过去。

本节要解决的问题 ​

这一节笔记的主题是:ref 与 v-for 结合使用时,this.$refs 里拿到的到底是什么。笔记里先放了两个普通 div 都写 ref="my" 的情况,又放了 p 元素使用 v-for 并写 ref="myP" 的情况。核心结论只有两条:

  • 如果多个普通元素的 ref 值相同,后者会覆盖前者。
  • 如果 ref 与 v-for 结合使用,获取到的是数组。

下面保留本节笔记代码,再按“为什么需要 → 怎么想 → 怎么做 → 易错点”拆开说明。

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 值会后者覆盖前者 ​

为什么需要:有时我们想通过 ref 快速拿到真实 DOM,比如按钮、输入框、某个容器。但 ref 的名字是开发者自己起的,如果随手起重复名字,就需要知道 Vue 会怎么处理。

怎么想:this.$refs 本质上是一个对象,对象里的属性名就是 ref 的值。普通元素不是 v-for 批量生成的,Vue 不会自动把它收集成数组;同一个属性名再次赋值,就会覆盖上一次的值。

怎么做:笔记中两个 div 都写了 ref="my":

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

点击按钮时如果输出 this.$refs.my,得到的是下面那个 div,也就是内容为 2 的元素。转写稿里的调试过程也是先写两个同名 ref,点击后发现拿到的是下面这个,说明后者覆盖了前者。

易错点:不要以为同名 ref 会自动变成数组;只有和 v-for 结合时才是数组。普通元素同名 ref 是覆盖关系,不是收集关系。

知识点二:ref 与 v-for 结合得到数组 ​

为什么需要:v-for 会渲染多个同类元素。比如列表里有 4 个 p,我们可能想拿到第 1 个元素的文本,或者批量操作这些 DOM。如果还按普通 ref 的思路,只能拿到一个,就不够用。

怎么想:v-for 生成几个元素,ref 就对应收集几个元素。Vue 对 v-for 里的 ref 做了特殊处理,把它们放进数组里。数组顺序对应渲染顺序。

怎么做:模板中写:

html
<p ref="myP" v-for="item in arr" :key="item">{{item}}</p>

数据是:

js
data:{
	arr:[1,2,3,4]
}

点击按钮执行:

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

this.$refs.myP 是数组,里面有 4 个 p 元素;[0] 取第一个,innerText 取文本,所以输出 1。转写稿里也验证了:输出 this.$refs.myP 能看到 4 个 P 元素,写下标 0 再取 innerText,得到的就是 1。

易错点:

  1. this.$refs.myP 是数组,不是单个 DOM,不能直接写 this.$refs.myP.innerText。
  2. 数组里元素的顺序跟 v-for 渲染顺序一致,按下标取值时要确认下标含义。
  3. v-for 要写 :key,笔记里用 :key="item",因为 item 在当前数组里唯一。
  4. 如果元素被 v-if 控制、或还没渲染完,this.$refs 可能拿不到,取值前要确认渲染时机。
  5. 不要和普通同名 ref 混淆:普通同名 ref 是覆盖,v-for 里的 ref 才是数组。

调试过程与结论 ​

转写稿的验证顺序很清楚:先写两个 div 都加 ref="my",点击按钮发现拿到的是下面那个,证明“后者覆盖前者”;再加 hr 和 v-for 的 p,给 p 加 ref="myP",点击按钮输出 this.$refs.myP,看到是 4 个 P 元素,取 [0].innerText 得到 1。这说明 ref 与 v-for 结合时,this.$refs.myP 是一个数组,数组元素就是 v-for 出来的元素。

本节记住两句话即可:普通元素同名 ref 后者覆盖前者;ref 与 v-for 结合获取到的是数组。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:动态组件 <component :is="..."> ​

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="changeIndex">切换</button>
	<!--使用动态组件:component是内置组件,is属性的值是要使用的组件的名字-->
	<component :is="arr[index]"></component>

	<!--未使用动态组件时,要写一堆分支-->
	<!--<one v-if="index===0"></one>-->
	<!--<two v-else-if="index===1"></two>-->
	<!--<three v-else-if="index===2"></three>-->
	<!--<four v-else-if="index===3"></four>-->

	<!--这种只是“显示/隐藏”,并没有真正切换 component 所代表的组件-->
	<!--<component v-show="index===i" v-for="(item,i) in arr" :is="item" :key="i"></component>-->
</div>
</body>
<script>
	new Vue({
		el: "#app",
		data: {
			arr: ["one", "two", "three", "four"],
			index: 0
		},
		methods: {
			changeIndex() {
				this.index++;
				if (this.index > 3) this.index = 0;
			}
		},
		components: {
			One: {
				template: `<div style="background:red">one</div>`
			},
			Two: {
				template: `<div style="background:green">two</div>`
			},
			Three: {
				template: `<div style="background:yellow">three</div>`
			},
			Four: {
				template: `<div style="background:pink">four</div>`
			}
		}
	})
</script>
</html>

关键行与易错点

  • <component> 是 Vue 提供的内置组件,不用自己注册;真正渲染哪个组件由 is 属性的值决定。
  • :is="arr[index]" 前面的冒号不能丢:带冒号表示里面是表达式(变量),会随 index 变化重新渲染;写成 is="arr[index]" 则只是普通字符串,会被当成组件名去找,直接报错。
  • arr 里写的是 "one"、"two",而注册名是 One、Two,Vue 内部会做驼峰/首字母大写匹配,所以能对应上;但为了清晰,建议名字与注册名保持一致。
  • changeIndex 里用 if (this.index > 3) this.index = 0 做循环,否则 index 越界后 arr[index] 为 undefined,组件就渲染不出来了。
  • 对比写法:用 v-if / v-else-if 要写四个分支,组件多了非常啰嗦;用 v-for + v-show 只是控制显示隐藏,component 代表的组件并没有被替换,体现不出“动态组件”的意义。

示例二: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">
	<input type="text" ref="inp">
	<div ref="my">my</div>
	<button ref="btn" @click="fn">点我</button>
</div>
</body>
<script>
	// 1- 设置:为元素增加ref属性,值为字符串
	// 2- 获取:通过this.$refs.字符串 获取真实DOM
	new Vue({
		el: "#app",
		methods: {
			fn(e) {
				console.log(e.target.innerText);
				console.log(document.querySelector("button").innerText);
				console.log(this.$refs.btn.innerText);
				console.log(this.$refs.btn === document.querySelector("button")); // true
				console.log(this.$refs.my);
				this.$refs.my.style.color = "pink";
				console.log(this.$refs.inp.value);
			}
		}
	})
</script>
</html>

关键行与易错点

  • 设置:在元素上加 ref="名字",值是字符串(不带冒号);获取:this.$refs.名字。
  • this.$refs.btn === document.querySelector("button") 结果为 true,说明拿到的是真实 DOM,可以直接用原生 JS 改样式、取值。
  • 没设置过 ref 时,this.$refs 是一个空对象,取 this.$refs.xxx 得到 undefined,再点属性就会报错,使用前要确认元素已渲染。
  • 只有在 mounted 之后(或事件回调中)才能拿到 DOM;在 created 里取不到。
  • 想“收集表单值”这种只读一次的场景,用 ref 比 v-model 更轻;需要视图与数据双向同步时才用 v-model。

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

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)   // 只会得到最后一个 div(内容为 2)
				console.log(this.$refs.myP[0].innerText); // 1
				console.log(this.$refs.myP.length);       // 4
			}
		}
	})
</script>
</html>

关键行与易错点

  • 两个普通 div 都写了 ref="my",后写的会覆盖先写的,this.$refs.my 只能拿到第二个 div,不会报错但结果不是你以为的。
  • ref 与 v-for 结合时,拿到的是数组,数组元素就是循环出来的 DOM,顺序与循环顺序一致;用 this.$refs.myP[0] 访问第一个。
  • 这里和 React 的 ref 有区别:React 中 ref 与列表结合通常拿到的是最后一个,Vue 中是数组,面试常问。
  • v-for 一定要写 :key,否则会有警告,也可能导致 DOM 复用错乱,进而让 ref 数组的顺序与数据不一致。
  • 补充:ref 加在组件标签上时,this.$refs.xxx 拿到的是组件实例,可以直接访问/修改该子组件的 data 和方法,实现父组件操作子组件。

常见错误 ​

1. :is 漏写冒号,组件不渲染

  • 现象:页面只出现空白,控制台报 Unknown custom element: <one>,标签原样留在 DOM 里。
  • 原因:写成 is="arr[index]",is 被当成普通字符串属性,Vue 把 arr[index] 这串字面量当作标签名解析。
  • 修法:改成 :is="arr[index]"(或 v-bind:is="arr[index]"),让表达式求值后再交给内置组件。

2. :is 的值与 components 注册名对不上

  • 现象:data 里写 ["one","two"],components 里注册的是 One、Two,切换时整块区域消失或报警告。
  • 原因:is 的值是运行时按名字查表的,查不到就退化成原生标签渲染;注册键的大小写与使用值不一致。
  • 修法:两边保持完全一致——要么注册键写成 'one'(加引号),要么数组里存 "One"。团队约定统一用大写驼峰注册、is 值同形。

3. 切换回来组件状态被清空

  • 现象:在 one 里输入了内容、点了计时器,切到 two 再切回 one,输入框空了、计时器从头开始。
  • 原因:<component :is> 切换时旧组件被销毁、新组件被创建,生命周期重跑,data 重置。
  • 修法:用 <keep-alive> 包住动态组件缓存实例;若业务本就要求"多面板并存只显示一个",改用 v-for + v-show 的方案。

4. v-for 渲染多个动态组件时忘记 key

  • 现象:控制台提示需要唯一 key,切换时偶尔出现内容串台、颜色错位。
  • 原因:同位置复用同一批 component 节点,Vue 无法区分哪个实例对应哪个 is 值。
  • 修法::key="item" 或 :key="i",保证每个动态组件有稳定唯一标识。

5. 同名 ref 挂在多个普通元素上,只拿到最后一个

  • 现象:两个 div 都写 ref="my",this.$refs.my 只指向第二个。
  • 原因:ref 不是数组容器,同名时后注册覆盖先注册。
  • 修法:普通元素的 ref 名保持唯一;确实需要批量获取时,改用 ref + v-for(此时 $refs.xxx 自动是数组),再按索引取用。

6. 把组件的 ref 当 DOM 用

  • 现象:给子组件加 ref="card" 后写 this.$refs.card.innerText 得到 undefined,或 .style 报错。
  • 原因:ref 打在组件标签上,拿到的是组件实例,不是根 DOM。
  • 修法:要 DOM 用 this.$refs.card.$el;要数据用 this.$refs.card.name;要方法直接 this.$refs.card.fn()。另外注意在 mounted 之前(如 created)访问一律是 undefined。

练习 ​

1. 轮播式切换器 用 component :is 实现 4 个不同背景色组件的循环切换,点击按钮一轮回。要求 index 越界时用取模处理而不是 if 判断,并保证连点 100 次不出空白。

2. 加缓存的动态组件 在第 1 题基础上,给 one 组件加一个 input 和一个"开始计时"按钮(setInterval 累加秒数)。验证:切走再切回,输入内容与秒数是否还在?若丢失,用 <keep-alive> 修复,并说明为什么 v-show 方案也能达到同样效果。

3. ref 批量取值 渲染 5 个 p 标签(v-for + ref="items"),点击按钮输出"第 3 个元素的文本"和"元素总数"。再故意把 ref 换成不带 v-for 的两个同名 div,观察结果差异并解释。

4. 父组件操作子组件 写一个子组件 Counter(内部有 count 和 add() 方法),父组件放两个按钮:"读子组件的 count"和"调用子组件的 add()",全部通过 ref 完成,不使用 props / $emit。再补一个按钮直接改子组件根节点的 style.background。

5. 排错题 下面代码点按钮后区域一片空白,请指出至少两处问题并改正:

html
<div id="app">
  <button @click="index++">切换</button>
  <component is="arr[index]"></component>
</div>
<script>
new Vue({
  el: "#app",
  data: { arr: ["one", "two"], index: 0 },
  components: {
    One: { template: "<div>one</div>" },
    Two: { template: "<div>two</div>" }
  }
});
</script>

原视频对照 ​

#视频时长
1局部组件使用范围07:39
2全局组件06:05
3全局组件抽离13:08
4动态组件14:57
5ref与普通元素结合使用07:27
6ref与v-for结合使用05:06
7ref与组件结合使用04:59

小结 ​

  1. <component :is="..."> 的 is 必须绑定表达式,值就是"要渲染的组件名",写死字符串等于写死一个组件,写变量才是动态。
  2. 动态组件的本质是"同一个挂载点换实例":切换即销毁重建,状态天然不保留;要保留就上 keep-alive。
  3. 用 v-if / v-else-if 链手写切换能实现同样效果,但组件一多就退化成体力活,且容易漏掉兜底分支导致空白;动态组件把"选择"收敛到一个变量上。
  4. v-for + component :is 是"全部渲染出来"的思路,配 v-show 控制显隐;它与单个 component :is 的本质区别是:前者不换实例,后者换实例。
  5. ref 的三条取值规则要记牢:普通元素 → DOM;v-for 上 → 数组;组件上 → 组件实例(DOM 需 .$el)。
  6. 同名 ref 在普通元素上后覆盖前,是排查"拿到的元素不对"的第一嫌疑;批量场景请直接走 v-for。
  7. ref 是"绕过数据流直接拿实例"的后门,适合表单取值、聚焦、播放器控制、父调子方法这类命令式操作;能用 props/$emit 表达的数据流不要用它,否则父子耦合会失控。
  8. 所有 $refs 的访问时机都在挂载之后——mounted 及事件回调里安全,created 里必然是 undefined。