Skip to content

具名插槽与作用域插槽(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 上。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:具名插槽(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 的体验优势。

原视频对照 ​

#视频时长
1v-model与vuex中的数据状态实现双向06:27
2缓存组件20:10
3实现懒加载11:46
4小说列表06:42
5小说列表-使用组件04:43
6匿名插槽06:19
7具名插槽06:13

小结 ​

  1. 插槽解决的是「子组件定结构、父组件定内容」的分工问题:匿名插槽给默认位置,具名插槽给指定位置,作用域插槽把子组件的数据反向交给父组件渲染。
  2. #name 就是 v-slot:name 的缩写;组件标签上的 v-slot 只能承载默认插槽,多个具名插槽必须用 <template> 分包。
  3. 一旦插槽带上 name,未被任何 v-slot 认领的内容就不会渲染——名字对不上是最常见的「内容消失」原因。
  4. v-model 绑 Vuex 必须走「计算属性 get/set + mutation」这条链路,直接改 state 在严格模式下必报错,且破坏单向数据流。
  5. keep-alive 的 include/exclude 匹配的是组件 name,不是路由 name;组件没写 name 时该配置静默失效。
  6. activated / deactivated 只在组件被 keep-alive 缓存后才有意义,可用于「隔一段时间再进来时刷新数据」这类场景,与 mounted 的职责要分清。
  7. 路由懒加载把「一次性全量加载」变成「按需加载」,本质是牺牲首次点击的微小延迟换取首屏体积与服务器压力;webpackChunkName 相同才会合并为同一个 chunk。
  8. 缓存、懒加载、插槽三者常在同一页面叠加使用:插槽负责结构复用,懒加载负责加载时机,keep-alive 负责状态留存——排错时按「加载了没 → 缓存了没 → 插槽名对上了没」的顺序定位最快。