1 插槽
组件封装的一把利器
1.1 是什么
插槽,也就是slot,它相当于一个占位符,允许父组件向子组件插入一些内容,这些内容在子组件中的位置就由slot来决定。
实用场景,根据业务对组件库中的组件进行二次封装,一般组件库中提供的组件都会留下可灵活插入内容的插槽,可以让使用者自定义某些内容。

1.2 怎么用
- 匿名插槽
匿名插槽就是一个没有name属性的插槽。父组件中除了具名包裹内容外,其他内容全部插入到匿名插槽中。
匿名插槽虽然没有name属性,但出口会带有一个隐含的名字“default”。单独使用匿名插槽时,父组件中可以不标注default,但使用多个插槽时,最好以名字区分插槽。
<!-- 子组件 -->
<template>
<div class="child">
<h3>子组件:匿名插槽</h3>
<slot></slot>
</div>
</template>
<!-- 父组件 -->
<template>
<div class="parent">
<child>
<template>
<p>插入匿名插槽</p>
</template>
</child>
</div>
</template>
- 具名插槽
具名插槽就是一个有name属性的插槽。
<!-- 子组件 -->
<template>
<div class="child">
<h3>子组件:具名插槽</h3>
<slot name="child"></slot>
</div>
</template>
<!-- 父组件1:vue2.6之后的写法 -->
<template>
<div class="parent">
<child>
<template v-slot:child>
<p>插入具名插槽</p>
</template>
</child>
</div>
</template>
<!-- 父组件2:v-slot的简写 -->
<template>
<div class="parent">
<child>
<template #child>
<p>插入具名插槽</p>
</template>
</child>
</div>
</template>
<!-- 父组件3:vue2.6之前的写法 -->
<template>
<div class="parent">
<child>
<p slot="child">插入具名插槽</p>
</child>
</div>
</template>
v-slot和slot的用法还是有区别的,v-slot只能用于template和组件,而slot属性可以加在任意标签上。
- 作用域插槽
只有匿名插槽和具名插槽是远远不够的,这两种插槽仅仅只能把Dom插入到封装好的组件中,而不能获取组件中的数据。
我们的业务需求中需要定制或限制数据展示、组件内部提供一个函数方法去让外部调用修改。这就需要我们在父组件中操作这些数据,要获取子组件中的数据,就要使用作用域插槽。
<!-- 子组件 -->
<template>
<div class="child">
<h3>子组件:作用域插槽</h3>
<slot name="user" :user="user"></slot>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: 'zhunny',
age: 26
}
}
}
}
</script>
<!-- 父组件1 vue2.6之后的用法 -->
<template>
<div class="parent">
<child>
<template #user="{ user }">
<p>hi,my name is {{ user.name }}, i am {{age}} years old</p>
</template>
</child>
</div>
</template>
<!-- 父组件2 vue2.6之前的用法 -->
<template>
<div class="parent">
<child>
<template slot="user" slot-scope="user">
<p>hi,my name is {{ user.name }}, i am {{age}} years old</p>
</template>
</child>
</div>
</template>
作用域插槽使用v-bind绑定需要暴露出去的数据。
1.3 JSX中使用插槽
JSX中是没有v-slot指令的
假设父组件和子组件全部使用JSX来实现:
- 匿名插槽和具名插槽
<!-- 父组件使用elementui的弹窗组件,省略了不必要的属性 -->
render() {
return (
<el-dialog title="弹框标题">
{/*这里就是默认插槽*/}
<div>这里是弹框内容</div>
{/*这里就是具名插槽*/}
<template slot="footer">
<el-button>确定</el-button>
<el-button>取消</el-button>
</template>
</el-dialog>
)
}
<!-- 子组件即Dialog的内部实现 -->
render() {
return (
<div>
{/**自定义匿名插槽*/}
{this.$slots.default}
{/**自定义具名插槽*/}
<div>{this.$slots.footer}</div>
</div>
)
}
- 作用域插槽
<!-- 父组件使用elementui的Table组件,自定义列表和表头 -->
render() {
return (
<el-table data="this.data">
{/*这里就是作用域插槽*/}
<el-table-column
scopedSlots={{
default: ({ row, $index }) => {
return <el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">
> 删除
</el-button>
},
header: () => {
return <span style="color:red">操作</span>
}
}}
></el-table-column>
</el-table>
)
}
<!-- 子组件即Table-Column的内部实现,表内容与title同理 -->
render() {
const { data } = this
// 获取标题作用域插槽
const titleSlot = this.$scopedSlots.title
return (
<div>
{/** 如果有标题插槽,则使用标题插槽,否则使用默认标题 */}
{titleSlot ? titleSlot(data) : <span>{data.title}</span>}
</div>
)
}
1.4 比较$scopeSlots和$slots
vue2.6之后,$scopeSlots数组中包含了所有类型的插槽,数组中的元素的类型是一个函数,通过调用函数生成一个VNode,渲染到页面上,函数的入参是需要传递的数据。
$slots中只有匿名插槽和具名插槽,它的元素类型就只是一个VNode。
但是有情况是特殊的,存在同名的作用域插槽和具名插槽:
<!-- 父组件 -->
<template>
<div class="parent">
<child>
{/** 插入匿名插槽 */}
插入匿名插槽
{/** 插入名为default的作用域插槽 */}
<template #default="{ test }">
hi,{{ test }}
</template>
</child>
</div>
</template>
<!-- 子组件 -->
render() {
return (<div>
<p>{this.$scopedSlots.default({ test: this.test })}</p>
<p>{this.$slots.default}</p>
</div>)
},
data() {
return {
test: '测试'
}
}
这种情况下,$scopeSlot中的default为作用域插槽,$slots中的default为匿名插槽。

<!-- 父组件 -->
<template>
<div class="parent">
<child>
{/** 插入匿名插槽 */}
插入匿名插槽
</child>
</div>
</template>
<!-- 子组件 -->
render() {
return (<div>
<p>{this.$scopedSlots.default({ test: this.test })}</p>
<p>{this.$slots.default}</p>
</div>)
},
data() {
return {
test: '测试'
}
}
而不存在同名的具名插槽和作用插槽时,$scopeSlot中的default和$slots中的default为同一个插槽。

<!-- 父组件 -->
<template>
<div class="parent">
<child>
{/** 插入名为default的作用域插槽 */}
<template #default="{ test }">
hi,{{ test }}
</template>
</child>
</div>
</template>
<!-- 子组件 -->
render() {
return (<div>
<p>{this.$scopedSlots.default({ test: this.test })}</p>
<p>{this.$slots.default}</p>
</div>)
},
data() {
return {
test: '测试'
}
}

1.5 参考资料
- 学会使用Vue JSX,一车老干妈都是你的:juejin.cn/post/684668…
- Vue组件封装利器——slot插槽:juejin.cn/post/685823…