主题
具名插槽与作用域插槽(Lesson13 2/3)
本页对应 Lesson13,共 7 段视频,约 1 小时 2 分。
学习目标
- 能使用计算属性的 getter/setter 将
v-model与 Vuex 中的状态进行双向同步,并验证输入变化会触发对应 mutation/action 更新。 - 能使用
keep-alive缓存路由组件,并通过include、exclude或路由meta配置精确控制需要缓存和排除的组件。 - 能使用
activated与deactivated钩子处理缓存组件的进入和离开逻辑,并说明其与mounted的执行时机差异。 - 能使用动态
import()实现路由懒加载,并通过webpackChunkName将指定组件打包到同一 chunk 中。 - 能将小说列表抽取为可复用组件,通过
props接收数据并正确渲染列表内容。 - 能区分并使用匿名插槽、具名插槽和作用域插槽,通过
slot、v-slot或#语法完成内容分发及子组件数据暴露。
知识点
本节要解决的问题
前面用组件渲染小说列表时,男生列表和女生列表结构相似,但标题文字、标题颜色不同。若继续用 props 传标题、颜色,甚至以后还要传背景图,父组件要传的字段会越来越多,子组件也要不断加 props,很麻烦。具名插槽解决的是:子组件只负责占位,父组件把一段结构“投递”到指定名字的插槽里。这样标题、样式、标签结构都由父组件决定,子组件保持稳定。
子组件占位:slot 与 name
为什么需要:子组件要知道“父组件传进来的内容放在哪里”。
怎么想:在子组件模板里放一个内置 <slot> 组件占位;如果希望接收多块内容,就给 slot 起名字。
怎么做:在 BookList.vue 中写 <slot name="one"></slot>,表示这里专门接收名字为 one 的插槽内容;下面的 v-for 仍负责渲染 bookList。
易错点:name 是插槽名字,不是组件名;父组件写的名字必须和这里完全一致。若子组件没有 name="one",父组件写 v-slot:one 的内容不会显示。转写稿里把“插槽”说成“插头”,这里统一纠正为“插槽”。
父组件投递:v-slot:one 与 #one
为什么需要:父组件要把“男生爱看”“女生爱看”分别放进不同位置。
怎么想:在子组件标签上使用 v-slot:插槽名,标签内部包裹的内容就会进入对应插槽。
怎么做:<BookList v-slot:one :bookList="boy"> 内部放 <h3>男生爱看</h3>;另一个用简写 <BookList #one :bookList="girl">,内部放女生标题。#one 就是 v-slot:one 的简写。
易错点:v-slot:one 要写在组件标签或 <template> 上,不要写到普通 div 上;# 后跟插槽名,不要漏写名字。转写稿中先只保留男生列表,加 v-slot:one 后页面出现包裹内容;把 one 改成 to 后不显示,因为子组件没有 name="to",这说明名字必须对应。转写稿还出现 v-slot 拼写错误导致模板报错,清理无关内容后才定位到指令拼写和标签闭合问题。
完整代码:App.vue 与 BookList.vue
vue
<template>
<div>
<!-- 将包裹的内容放置到名字为one的插槽中 -->
<BookList v-slot:one :bookList="boy">
<h3 style="color:blue;">男生爱看</h3>
</BookList>
<hr/>
<!-- v-slot:可以用#代替 -->
<BookList #one :bookList="girl">
<h3 style="color:pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>vue
<template>
<div>
<slot name="one"></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{item.bookName}}》-->{{item.author}}</p>
</div>
</div>
</template>
<script>
export default {
name: "BookList",
props:["bookList"]
}
</script>
<style scoped>
</style>调试与验证:名字必须对上
为什么需要:插槽内容不显示时,最常见原因不是数据错,而是名字没对上。
怎么想:先看子组件有没有 <slot name="one">,再看父组件是不是 v-slot:one 或 #one。
怎么做:转写稿中的验证顺序是:先只保留男生列表,父组件写 v-slot:one,内部先放 <p>1</p>,页面出现“1”;再换成 <h3>男生爱看</h3>,结构也能进入插槽;最后把 v-slot:one 改成 #one,效果不变。
易错点:多个具名插槽时,每块内容都要写对应名字;没有名字的 slot 是匿名插槽,只接收未指定名字的内容。具名插槽不是“随便放进去就显示”,而是“按名字投递”。模板报错时先检查 v-slot 拼写、冒号、# 简写和标签闭合,再检查插槽名。
作用域插槽要解决什么问题
为什么需要:普通插槽解决的是“父组件决定往子组件里塞什么内容”,但插槽内容默认处在父组件作用域里,拿不到子组件内部的 data。本节 BookList 里有 a、b、arr,父组件如果只写普通插槽,就无法直接渲染这些子组件数据。
怎么想:既然数据在子组件里,就让子组件在 <slot> 上把数据“暴露”出来;父组件在使用插槽时接收这些数据。
怎么做:子组件写 <slot :a="a" :b="b" :arr="arr"></slot>,父组件通过 v-slot 接收。
易错点:不要把 slotProps.a 直接写在子组件里;它是父组件插槽内容中使用的对象。
子组件如何暴露数据
为什么需要:作用域插槽的核心是“子组件传数据给插槽内容”。
怎么想:<slot> 不只是占位,还可以像组件传 props 一样绑定属性。
怎么做:本节 BookList.vue 中保留如下结构:
vue
<template>
<div>
<slot :a="a" :b="b" :arr="arr"></slot>
</div>
</template>
<script>
export default {
name: "BookList",
data(){
return {
a:1,
b:2,
arr:[1,2,3,4,5]
}
},
props:["bookList"]
}
</script>
<style scoped>
</style>易错点:slot 上绑定的 :a、:b、:arr 是给父级插槽内容用的,不是 BookList 的 props;不要和 props:["bookList"] 混淆。
父组件三种接收写法
为什么需要:笔记里特意保留了三种写法,是为了说明默认插槽的几种等价写法。
怎么想:v-slot="slotProps" 是默认插槽简写;v-slot:default="slotProps" 明确写默认插槽;#default="slotProps" 是 v-slot:default 的简写。
怎么做:本节最终启用第 3 种:
vue
<template>
<div>
<!-- 1-->
<!-- <BookList v-slot="slotProps">-->
<!-- <div>a:{{slotProps.a}}</div>-->
<!-- <div>b:{{slotProps.b}}</div>-->
<!-- <div>arr:{{slotProps.arr}}</div>-->
<!-- </BookList>-->
<!-- 2 -->
<!-- <BookList v-slot:default="slotProps">-->
<!-- <div>a:{{slotProps.a}}</div>-->
<!-- <div>b:{{slotProps.b}}</div>-->
<!-- <div>arr:{{slotProps.arr}}</div>-->
<!-- </BookList>-->
<!-- 3-->
<BookList #default="slotProps">
<div>a:{{slotProps.a}}</div>
<div>b:{{slotProps.b}}</div>
<div>arr:{{slotProps.arr}}</div>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>易错点:slotProps 名字可以换,但前后要一致;#default 中 # 是 v-slot: 的简写,不能写成 #:default;默认插槽简写 v-slot="slotProps" 只适合只有一个默认插槽的场景。
调试与验证过程
转写稿里讲插槽时反复强调“子组件里用内置 slot 占位,占位呈现使用子组件时包裹的内容”。作用域插槽就在这个基础上多一步:把子组件数据交给包裹内容。调试时可以按笔记注释逐个切换:先放开写法 1,观察页面是否输出 a:1、b:2、arr:[1,2,3,4,5];再换成写法 2,结果应一致;最后启用写法 3,确认 #default="slotProps" 等价。若页面空白或报 slotProps 未定义,先检查父组件是否写了 v-slot/#default,再检查子组件 slot 上是否绑定了对应属性。
易错点清单
易错点一:把作用域插槽当成普通插槽,父组件直接写 {{a}},此时拿不到子组件 data。
易错点二:slotProps 是接收对象,里面才有 a、b、arr,不要写成 slotProps 本身。
易错点三:具名插槽用 #名字,默认插槽用 #default;名字必须和子组件 slot 的 name 对应。本节是默认插槽,所以写 #default。
易错点四:v-slot 要写在组件标签或 <template> 上,不要随便写在普通 div 上。
课堂画面






代码示例
示例一:具名插槽(v-slot:one / #one)
src/components/BookList.vue
vue
<template>
<div>
<slot name="one"></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{item.bookName}}》-->{{item.author}}</p>
</div>
</div>
</template>
<script>
export default {
name: "BookList",
props:["bookList"]
}
</script>
<style scoped>
</style>src/App.vue
vue
<template>
<div>
<!-- 将包裹的内容放置到名字为one的插槽中 -->
<BookList v-slot:one :bookList="boy">
<h3 style="color:blue;">男生爱看</h3>
</BookList>
<hr/>
<!-- v-slot:可以用#代替 -->
<BookList #one :bookList="girl">
<h3 style="color:pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>关键行与易错点
<slot name="one"></slot>:子组件里用内置slot组件占位,name就是插槽的名字,这个位置最终渲染什么由“使用者”决定。v-slot:one/#one:两者完全等价,#是v-slot:的简写。冒号右边必须和子组件slot的name一一对应,对不上就不渲染(笔记里把#one改成#two而子组件没有name="two"时,内容直接不出现)。- 插槽内容写在组件标签的包裹内容里,不是通过 props 传;子组件不需要为标题写任何
props,比“传 title、传 color”灵活得多。 - 易错点:
v-slot只能写在<template>或组件标签上,写在普通<p>这类元素上会直接模板编译报错。 - 易错点:写了
name的插槽,只有显式指定该名字的内容才会进去;不写name的匿名插槽(默认插槽)才会“谁来都能占”。
示例二:作用域插槽(子组件把数据回传给插槽内容)
src/components/BookList.vue
vue
<template>
<div>
<slot :a="a" :b="b" :arr="arr"></slot>
</div>
</template>
<script>
export default {
name: "BookList",
data(){
return {
a:1,
b:2,
arr:[1,2,3,4,5]
}
},
props:["bookList"]
}
</script>
<style scoped>
</style>src/App.vue
vue
<template>
<div>
<!-- 1-->
<!-- <BookList v-slot="slotProps">-->
<!-- <div>a:{{slotProps.a}}</div>-->
<!-- <div>b:{{slotProps.b}}</div>-->
<!-- <div>arr:{{slotProps.arr}}</div>-->
<!-- </BookList>-->
<!-- 2 -->
<!-- <BookList v-slot:default="slotProps">-->
<!-- <div>a:{{slotProps.a}}</div>-->
<!-- <div>b:{{slotProps.b}}</div>-->
<!-- <div>arr:{{slotProps.arr}}</div>-->
<!-- </BookList>-->
<!-- 3-->
<BookList #default="slotProps">
<div>a:{{slotProps.a}}</div>
<div>b:{{slotProps.b}}</div>
<div>arr:{{slotProps.arr}}</div>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>关键行与易错点
<slot :a="a" :b="b" :arr="arr"></slot>:在子组件的slot上写绑定属性,这些属性会被“打包成一个对象”传给父级的插槽内容,这就是作用域插槽的核心——数据在子组件,结构在父组件。v-slot="slotProps":slotProps是接收到的对象名,名字可自定义;取用时必须写成slotProps.a,不能直接写a。- 三种写法等价:
v-slot="slotProps"、v-slot:default="slotProps"、#default="slotProps"。不写插槽名时默认就是default,所以示例中三个注释块效果一致。 - 易错点:
#default="slotProps"里的双引号不能丢,引号内是变量名;写成#default=slotProps或把v-slot放到普通div上都会报模板错误。 - 易错点:只有“默认插槽”才允许把
v-slot直接写在组件标签上;如果是具名插槽(如#one)又要同时接收数据,需用<template #one="slotProps">包一层。
常见错误
1. 具名插槽内容不显示,控制台报 v-slot can only be used on components or <template>
- 现象:
<BookList v-slot:one>里的内容整体不渲染,或直接把v-slot:one写在普通<div>、<h3>上。 - 原因:
v-slot只能挂在「组件标签」或<template>上,挂在原生元素上模板编译即失败。 - 修法:改成
<BookList v-slot:one>或<BookList><template #one>…</template></BookList>。
2. 一个组件上想同时给两个具名插槽传内容,结果只有一个生效或报错
- 现象:写了
<BookList v-slot:one>…</BookList>后,想再加v-slot:two无处可放。 - 原因:组件标签上的
v-slot只能声明「默认插槽」这一份,多个具名必须拆到 template 上。 - 修法:
<BookList><template #one>…</template><template #two>…</template></BookList>。
3. 作用域插槽里 slotProps 是空对象 / undefined
- 现象:
#default="slotProps"后slotProps.a取不到值。 - 原因:子组件只写了
<slot></slot>没有向上绑定属性;或父级用了#one而子组件插槽名是默认的。 - 修法:子组件
<slot :a="a" :b="b" :arr="arr">;父级插槽名与子组件name对齐(默认插槽用#default或省略)。
4. v-model 直接绑 $store.state.xxx,严格模式报 do not mutate vuex store state outside mutation handlers
- 现象:输入框能改,但控制台报错,且
state被绕过 mutation 直接篡改。 - 原因:
v-model本质是:value+@input赋值,赋值目标直接落在了state上。 - 修法:用计算属性做中转,
get返回this.$store.state.x,set(val)里this.$store.commit("updateXxx", val)。
5. keep-alive 加了但组件照样被销毁重建(表单内容丢失 / activated 不触发)
- 现象:
<keep-alive><router-view/></keep-alive>已写,切走再切回数据仍清空;activated/deactivated一次都不打印。 - 原因:① 用了
include/exclude,但里面写的是「路由 name」,而include匹配的是「组件的name选项」;② 组件压根没被缓存,这两个钩子自然不生效。 - 修法:给组件加
name: "Add"与include="Add"保持一致;或按路由meta.isKeep配合v-if包裹router-view,此时务必保证组件有name。
6. 路由懒加载改了写法,但打包后还是一个 app.js,或 chunk 名没合并
- 现象:改成
() => import(...)后 Network 里仍然一次性加载全部;或写了webpackChunkName却生成多个文件。 - 原因:① 只改了部分路由,其余仍是顶部静态
import(静态引入会被打进主包);② 魔法注释位置错,写在import外面或路径字符串后面;③ chunk 名拼写不一致(大小写、空格)。 - 修法:注释必须写在
import(的括号内、路径字符串之前:() => import(/* webpackChunkName: "group-se" */ "@/views/BookList");同名注释严格一致才会合并到同一个 JS。
7. 懒加载路径写错,切路由时报 Cannot find module,且开发环境不报错只白屏
- 现象:点击菜单白屏,控制台 chunk 加载 404。
- 原因:
@/views/xxx的目录或文件名大小写与实际不一致(Linux 构建机上尤其明显)。 - 修法:核对大小写与后缀,统一用
@/别名;构建后检查产物 chunk 名是否与预期一致。
练习
练习 1|具名插槽改造 把 BookList.vue 改为拥有 header、default、footer 三个插槽。父组件用 #header、#footer 简写传入标题和「共 N 本」统计,中间书单仍走默认插槽。
练习 2|作用域插槽自定义渲染 子组件除暴露 a、b、arr 外,再暴露 bookList 与每项的 index。父组件不写 v-for,改由插槽内容自行决定用 <ul>/<li> 还是表格渲染,并验证 slotProps 解构写法 #default="{ bookList }" 是否等价。
练习 3|v-model 与 Vuex 双向绑定 在 store 中放 state.name = "刘德华",写 mutation: updateName(state, val)。页面上放一个 input,用 v-model="comName" 实现双向绑定,计算属性 get/set 分别读 state 和 commit。再补一个 v-model.number 的数字字段,观察不加 .number 时 state 里存的是字符串还是数字。
练习 4|keep-alive 精准缓存 给 Add、Person 两个路由组件分别加 name,要求:Add 缓存、Person 缓存、其余不缓存。分别用 include 方案和「路由 meta.isKeep + v-if 包裹 router-view」方案各实现一次,并在 Add 中打印 mounted / activated / deactivated,说明三者执行顺序。
练习 5|懒加载与分包 把 5 个路由全部改为动态 import,要求把 BookList 和 Add 打进同一个名为 group-se 的 chunk,Person 单独命名。打包前后各截一次 Network 面板,对比首屏请求数量和主包体积,并解释为什么「首屏只加载首页」不违背 SPA 的体验优势。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | v-model与vuex中的数据状态实现双向 | 06:27 |
| 2 | 缓存组件 | 20:10 |
| 3 | 实现懒加载 | 11:46 |
| 4 | 小说列表 | 06:42 |
| 5 | 小说列表-使用组件 | 04:43 |
| 6 | 匿名插槽 | 06:19 |
| 7 | 具名插槽 | 06:13 |
小结
- 插槽解决的是「子组件定结构、父组件定内容」的分工问题:匿名插槽给默认位置,具名插槽给指定位置,作用域插槽把子组件的数据反向交给父组件渲染。
#name就是v-slot:name的缩写;组件标签上的v-slot只能承载默认插槽,多个具名插槽必须用<template>分包。- 一旦插槽带上
name,未被任何v-slot认领的内容就不会渲染——名字对不上是最常见的「内容消失」原因。 v-model绑 Vuex 必须走「计算属性 get/set + mutation」这条链路,直接改state在严格模式下必报错,且破坏单向数据流。keep-alive的include/exclude匹配的是组件name,不是路由name;组件没写name时该配置静默失效。activated/deactivated只在组件被keep-alive缓存后才有意义,可用于「隔一段时间再进来时刷新数据」这类场景,与mounted的职责要分清。- 路由懒加载把「一次性全量加载」变成「按需加载」,本质是牺牲首次点击的微小延迟换取首屏体积与服务器压力;
webpackChunkName相同才会合并为同一个 chunk。 - 缓存、懒加载、插槽三者常在同一页面叠加使用:插槽负责结构复用,懒加载负责加载时机,
keep-alive负责状态留存——排错时按「加载了没 → 缓存了没 → 插槽名对上了没」的顺序定位最快。