该页面假设你已经阅读过了组件基础 。如果你还对组件不太了解,推荐你先阅读它。
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中 的 attribute。新语法的由来可查阅这份 RFC 。
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案 ,将 <slot> 元素作为承载分发内容的出口。
它允许你像这样合成组件:
1 2 3 <navigation-link url ="/profile" > Your Profile </navigation-link >
然后你在 <navigation-link> 的模板中可能会写为:
1 2 3 4 5 6 <a v-bind:href ="url" class ="nav-link" > <slot > </slot > </a >
当组件渲染的时候,<slot></slot> 将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML:
1 2 3 4 5 <navigation-link url ="/profile" > <span class ="fa fa-user" > </span > Your Profile </navigation-link >
甚至其它的组件:
1 2 3 4 5 <navigation-link url ="/profile" > <font-awesome-icon name ="user" > </font-awesome-icon > Your Profile </navigation-link >
如果 <navigation-link> 的 template 中没有 包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
当你想在一个插槽中使用数据时,例如:
1 2 3 <navigation-link url ="/profile" > Logged in as {{ user.name }} </navigation-link >
该插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),而不能 访问 <navigation-link> 的作用域。例如 url 是访问不到的:
1 2 3 4 5 6 7 8 <navigation-link url ="/profile" > Clicking here will send you to: {{ url }} </navigation-link >
作为一条规则,请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:
1 2 3 <button type ="submit" > <slot > </slot > </button >
我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot> 标签内:
1 2 3 <button type ="submit" > <slot > Submit</slot > </button >
现在当我在一个父级组件中使用 <submit-button> 并且不提供任何插槽内容时:
1 <submit-button > </submit-button >
后备内容“Submit”将会被渲染:
1 2 3 <button type ="submit" > Submit </button >
但是如果我们提供内容:
1 2 3 <submit-button > Save </submit-button >
则这个提供的内容将会被渲染从而取代后备内容:
1 2 3 <button type ="submit" > Save </button >
自 2.6.0 起有所更新。已废弃的使用 slot attribute 的语法在这里 。
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:
1 2 3 4 5 6 7 8 9 10 11 <div class ="container" > <header > </header > <main > </main > <footer > </footer > </div >
对于这样的情况,<slot> 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽:
1 2 3 4 5 6 7 8 9 10 11 <div class ="container" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div >
一个不带 name 的 <slot> 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
1 2 3 4 5 6 7 8 9 10 11 12 <base-layout > <template v-slot:header > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template v-slot:footer > <p > Here's some contact info</p > </template > </base-layout >
现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
然而,如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <base-layout > <template v-slot:header > <h1 > Here might be a page title</h1 > </template > <template v-slot:default > <p > A paragraph for the main content.</p > <p > And another one.</p > </template > <template v-slot:footer > <p > Here's some contact info</p > </template > </base-layout >
任何一种写法都会渲染出:
1 2 3 4 5 6 7 8 9 10 11 12 <div class ="container" > <header > <h1 > Here might be a page title</h1 > </header > <main > <p > A paragraph for the main content.</p > <p > And another one.</p > </main > <footer > <p > Here's some contact info</p > </footer > </div >
注意 v-slot 只能添加在 <template> 上 (只有一种例外情况 ),这一点和已经废弃的 slot attribute 不同。
自 2.6.0 起有所更新。已废弃的使用 slot-scope attribute 的语法在这里 。
有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的 <current-user> 组件:
1 2 3 <span > <slot > {{ user.lastName }}</slot > </span >
我们可能想换掉备用内容,用名而非姓来显示。如下:
1 2 3 <current-user > {{ user.firstName }} </current-user >
然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user,而我们提供的内容是在父级渲染的。
为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:
1 2 3 4 5 <span > <slot v-bind:user ="user" > {{ user.lastName }} </slot > </span >
绑定在 <slot> 元素上的 attribute 被称为插槽 prop 。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
1 2 3 4 5 <current-user > <template v-slot:default ="slotProps" > {{ slotProps.user.firstName }} </template > </current-user >
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。
在上述情况下,当被提供的内容只有 默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:
1 2 3 <current-user v-slot:default ="slotProps" > {{ slotProps.user.firstName }} </current-user >
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
1 2 3 <current-user v-slot ="slotProps" > {{ slotProps.user.firstName }} </current-user >
注意默认插槽的缩写语法不能 和具名插槽混用,因为它会导致作用域不明确:
1 2 3 4 5 6 7 <current-user v-slot ="slotProps" > {{ slotProps.user.firstName }} <template v-slot:other ="otherSlotProps" > slotProps is NOT available here </template > </current-user >
只要出现多个插槽,请始终为所有的 插槽使用完整的基于 <template> 的语法:
1 2 3 4 5 6 7 8 9 <current-user > <template v-slot:default ="slotProps" > {{ slotProps.user.firstName }} </template > <template v-slot:other ="otherSlotProps" > ... </template > </current-user >
作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:
1 2 3 function (slotProps ) { }
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件 或现代浏览器 ),你也可以使用 ES2015 解构 来传入具体的插槽 prop,如下:
1 2 3 <current-user v-slot ="{ user }" > {{ user.firstName }} </current-user >
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:
1 2 3 <current-user v-slot ="{ user: person }" > {{ person.firstName }} </current-user >
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
1 2 3 <current-user v-slot ="{ user = { firstName: 'Guest' } }" > {{ user.firstName }} </current-user >
2.6.0 新增
动态指令参数 也可以用在 v-slot 上,来定义动态的插槽名:
1 2 3 4 5 <base-layout > <template v-slot: [dynamicSlotName ]> ... </template > </base-layout >
2.6.0 新增
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:
1 2 3 4 5 6 7 8 9 10 11 12 <base-layout > <template #header > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template #footer > <p > Here's some contact info</p > </template > </base-layout >
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
1 2 3 4 <current-user #="{ user }" > {{ user.firstName }} </current-user >
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
1 2 3 <current-user #default ="{ user }" > {{ user.firstName }} </current-user >
**插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。**这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。
例如,我们要实现一个 <todo-list> 组件,它是一个列表且包含布局和过滤逻辑:
1 2 3 4 5 6 7 8 <ul > <li v-for ="todo in filteredTodos" v-bind:key ="todo.id" > {{ todo.text }} </li > </ul >
我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <ul > <li v-for ="todo in filteredTodos" v-bind:key ="todo.id" > <slot name ="todo" v-bind:todo ="todo" > {{ todo.text }} </slot > </li > </ul >
现在当我们使用 <todo-list> 组件的时候,我们可以选择为 todo 定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取数据:
1 2 3 4 5 6 <todo-list v-bind:todos ="todos" > <template v-slot:todo ="{ todo }" > <span v-if ="todo.isComplete" > ✓</span > {{ todo.text }} </template > </todo-list >
这只是作用域插槽用武之地的冰山一角。想了解更多现实生活中的作用域插槽的用法,我们推荐浏览诸如 Vue Virtual Scroller 、Vue Promised 和 Portal Vue 等库。
v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slot 和 slot-scope attribute 的 API 替代方案。v-slot 完整的由来参见这份 RFC 。在接下来所有的 2.x 版本中 slot 和 slot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中。
自 2.6.0 起被废弃。新推荐的语法请查阅这里 。
在 <template> 上使用特殊的 slot attribute,可以将内容从父级传给具名插槽 (把这里 提到过的 <base-layout> 组件作为示例):
1 2 3 4 5 6 7 8 9 10 11 12 <base-layout > <template slot ="header" > <h1 > Here might be a page title</h1 > </template > <p > A paragraph for the main content.</p > <p > And another one.</p > <template slot ="footer" > <p > Here's some contact info</p > </template > </base-layout >
或者直接把 slot attribute 用在一个普通元素上:
1 2 3 4 5 6 7 8 <base-layout > <h1 slot ="header" > Here might be a page title</h1 > <p > A paragraph for the main content.</p > <p > And another one.</p > <p slot ="footer" > Here's some contact info</p > </base-layout >
这里其实还有一个未命名插槽,也就是默认插槽 ,捕获所有未被匹配的内容。上述两个示例的 HTML 渲染结果均为:
1 2 3 4 5 6 7 8 9 10 11 12 <div class ="container" > <header > <h1 > Here might be a page title</h1 > </header > <main > <p > A paragraph for the main content.</p > <p > And another one.</p > </main > <footer > <p > Here's some contact info</p > </footer > </div >
自 2.6.0 起被废弃。新推荐的语法请查阅这里 。
在 <template> 上使用特殊的 slot-scope attribute,可以接收传递给插槽的 prop (把这里 提到过的 <slot-example> 组件作为示例):
1 2 3 4 5 <slot-example > <template slot ="default" slot-scope ="slotProps" > {{ slotProps.msg }} </template > </slot-example >
这里的 slot-scope 声明了被接收的 prop 对象会作为 slotProps 变量存在于 <template> 作用域中。你可以像命名 JavaScript 函数参数一样随意命名 slotProps。
这里的 slot="default" 可以被忽略为隐性写法:
1 2 3 4 5 <slot-example > <template slot-scope ="slotProps" > {{ slotProps.msg }} </template > </slot-example >
slot-scope attribute 也可以直接用于非 <template> 元素 (包括组件):
1 2 3 4 5 <slot-example > <span slot-scope ="slotProps" > {{ slotProps.msg }} </span > </slot-example >
slot-scope 的值可以接收任何有效的可以出现在函数定义的参数位置上的 JavaScript 表达式。这意味着在支持的环境下 (单文件组件 或现代浏览器 ),你也可以在表达式中使用 ES2015 解构 ,如下:
1 2 3 4 5 <slot-example > <span slot-scope ="{ msg }" > {{ msg }} </span > </slot-example >
使用这里 描述过的 <todo-list> 作为示例,与它等价的使用 slot-scope 的代码是:
1 2 3 4 5 6 <todo-list v-bind:todos ="todos" > <template slot ="todo" slot-scope ="{ todo }" > <span v-if ="todo.isComplete" > ✓</span > {{ todo.text }} </template > </todo-list >
🧪 动手 Demo
🧪 最简单的例子(默认插槽)
这是一个**“卡片组件”**,卡片边框固定,但里面的内容让父组件随意填。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > Vue 2 插槽 - 极简入门</title > <script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/vue.js" > </script > <style > .card { border : 2px solid #4dabf7 ; border-radius : 8px ; padding : 15px ; margin : 10px 0 ; background : #f8f9fa ; } .card-title { font-weight : bold; color : #1c7ed6 ; border-bottom : 1px solid #dee2e6 ; padding-bottom : 8px ; margin-bottom : 10px ; } </style > </head > <body > <div id ="app" > <my-card > <p > 🔥 这是第一张卡片的内容:今天天气真好!</p > </my-card > <my-card > <button @click ="alert('点击了按钮')" > 点我试试</button > <span style ="color:green;" > 还能放按钮和文字混合哦</span > </my-card > <my-card > </my-card > </div > <script > // 1. 定义子组件(毛坯房) Vue.component('my-card', { template: ` <div class ="card" > <div class ="card-title" > 📦 卡片标题(固定结构)</div > <slot > (这里是备用内容,如果父组件没填东西,就显示这句)</slot > </div > ` }); new Vue({ el: '#app', methods: { alert(msg) { alert('父组件的方法被触发了!'); } } }); </script > </body > </html >
🧩 如果你需要“多个坑”(具名插槽)
有时候,卡片需要有头部 、正文 、底部 三个坑。这时候用 name 属性区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > 具名插槽示例</title > <script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/vue.js" > </script > <style > .layout { border :1px solid #ccc ; padding :10px ; margin :10px 0 ; } .header { background :#e3fafc ; padding :5px ; } .footer { background :#fcc2d7 ; padding :5px ; } </style > </head > <body > <div id ="app" > <base-layout > <template #header > <h3 > 🏷️ 这是头部(来自父组件)</h3 > </template > <p > 📝 这是主要内容区域(来自父组件)</p > <template #footer > <span > © 2026 底部版权(来自父组件)</span > </template > </base-layout > </div > <script > Vue.component('base-layout', { template: ` <div class ="layout" > <div class ="header" > <slot name ="header" > 默认头部</slot > </div > <div style ="padding:10px; background:white;" > <slot > 默认正文</slot > </div > <div class ="footer" > <slot name ="footer" > 默认底部</slot > </div > </div > ` }); new Vue({ el: '#app' }); </script > </body > </html >
总结
插槽:父组件直接把写好的 HTML 结构和逻辑(比如带 @click 的按钮)传给子组件,子组件只负责放在哪里,具体长什么样由父组件说了算。
所以你记住一句话:当你封装一个组件(如弹窗、卡片、列表容器),但不确定里面的具体内容时,就用插槽给用户留白。 这就是插槽存在的全部意义。上手试一试,你立刻就记住了!😊
参考
介绍 — Vue.js