主题
动态组件与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 的管理范围,也不容易因为页面里有多个同类元素而选错。转写稿里也强调,这种用法在实际开发中“用得比较多”。
核心思路:先设置,再获取
可以把它记成两步:
- 设置:为普通元素增加
ref属性,值是字符串,例如ref="btn"。 - 获取:在 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。
易错点:
this.$refs.myP是数组,不是单个 DOM,不能直接写this.$refs.myP.innerText。- 数组里元素的顺序跟
v-for渲染顺序一致,按下标取值时要确认下标含义。 v-for要写:key,笔记里用:key="item",因为item在当前数组里唯一。- 如果元素被
v-if控制、或还没渲染完,this.$refs可能拿不到,取值前要确认渲染时机。 - 不要和普通同名
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 结合获取到的是数组。
课堂画面






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