使用方法
用于只有一个插槽的情况:
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<slot></slot>
</button>
<todo-button>
<!-- 添加一个Font Awesome 图标 -->
<i class="fas fa-plus"></i>
Add todo
</todo-button>
具名插槽
用于多个插槽的情况:
<!--组件模版-->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!--用法-->
<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>
v-slot
也可简写成#
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>
作用域插槽
作用域插槽
可以给插槽传入组件的作用域参数
<!-- 组件模版 -->
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
<!-- 调用组件 -->
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
也可使用es6的解构语法解构插槽
<!-- 默认 -->
<todo-list v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
<!-- 重命名 -->
<todo-list v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</todo-list>
<!-- 默认值 -->
<todo-list v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
也可用动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
v-slot 或 # 只能写在template上,除非只有一个默认插槽,可以写在组件标签上
<base-layout #default>
<h1>Here might be a page title</h1>
</base-layout>
底层实现
先看下下面代码生成的render函数
<layout>
<template v-slot:header="slotScope">
<h1>{{slotScope.item}}</h1>
</template>
<template v-slot:default>
<p>{{ main }}</p>
</template>
<template v-slot:footer>
<p>{{ footer }}</p>
</template>
</layout>
import { toDisplayString, createVNode, resolveComponent, withCtx, openBlock, createBlock } from "vue"
// Binding optimization for webpack code-split
const _toDisplayString = toDisplayString, _createVNode = createVNode, _resolveComponent = resolveComponent, _withCtx = withCtx, _openBlock = openBlock, _createBlock = createBlock
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_layout = _resolveComponent("layout")
return (_openBlock(), _createBlock(_component_layout, null, {
header: _withCtx(() => [
_createVNode("h1", null, _toDisplayString(_ctx.slotScope.item), 1 /* TEXT */)
]),
default: _withCtx(() => [
_createVNode("p", null, _toDisplayString(_ctx.main), 1 /* TEXT */)
]),
footer: _withCtx(() => [
_createVNode("p", null, _toDisplayString(_ctx.footer), 1 /* TEXT */)
]),
_: 1 /* STABLE */
}))
}
从生成代码中可以看到使用createBlock
构建layout
组件的时候,第三个参数是一个对象。
而createBlock
实际调用的是createVNode
,createVNode
内部会对children
做格式化处理:
function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
...
if (typeof children === 'object') {
...
type = ShapeFlags.SLOTS_CHILDREN
...
} else if (isFunction(children)) {
...
} else {
...
}
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
此时将layout
的vnode.shapeFlag
和ShapeFlags.SLOTS_CHILDREN
执行了或操作(后续可以根据SLOTS_CHILDREN
做类型匹配):
再来看下组件的渲染流程:
在创建完vnode之后,因为layout
是一个component,所以后续patch
过程会走到processComponent
方法中, 从上图可以看出会在setupComponent
过程中执行initSlots
const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
// 这里就是createVNode处理的shapeFlag
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
instance.slots = children as InternalSlots
} else {
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
}
} else {
...
}
}
initSlots
很简单,就是把children
赋值给instance.slots
然后看下layout
组件的内容:
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
import { renderSlot, createVNode, openBlock, createBlock } from "vue"
// Binding optimization for webpack code-split
const _renderSlot = renderSlot, _createVNode = createVNode, _openBlock = openBlock, _createBlock = createBlock
const _hoisted_1 = { class: "layout" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createVNode("header", null, [
_renderSlot(_ctx.$slots, "header")
]),
_createVNode("main", null, [
_renderSlot(_ctx.$slots, "default")
]),
_createVNode("footer", null, [
_renderSlot(_ctx.$slots, "footer")
])
]))
}
这里会执行renderSlot
渲染slot的内容。(这个_ctx.$slots
就是instance.slots
,是在setupStatefulComponent
处做的代理访问)
function renderSlot(
slots: Slots,
name: string,
props: Data = {},
fallback?: () => VNodeArrayChildren
): VNode {
let slot = slots[name]
isRenderingCompiledSlot++
const rendered = (openBlock(),
createBlock(
Fragment,
{ key: props.key },
slot ? slot(props) : fallback ? fallback() : [],
(slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL
))
isRenderingCompiledSlot--
return rendered
}
renderSlot
调用了createBlock
创建了slot的vnode节点,后续通过processFragment
将slot的内容渲染在子组件对应的节点中。
作用域控制
我们都知道,插槽中访问的是父组件的作用域,这个是怎么实现的呢?
在上面可以看到,父组件生成render的时候,通过_withCtx
对slot的createVNode
做了一个包装,看一下_withCtx
做了什么:
function withCtx(
fn: Slot,
ctx: ComponentInternalInstance | null = currentRenderingInstance
) {
if (!ctx) return fn
const renderFnWithContext = (...args: any[]) => {
if (!isRenderingCompiledSlot) {
openBlock(true /* null block that disables tracking */)
}
// 暂存子组件的实例
const owner = currentRenderingInstance
// 将当前渲染的instance设置为父组件的实例
setCurrentRenderingInstance(ctx)
// 执行slot,此时获取到的instace为父组件的实例
const res = fn(...args)
// 还原当前渲染的instance
setCurrentRenderingInstance(owner)
if (!isRenderingCompiledSlot) {
closeBlock()
}
return res
}
renderFnWithContext._c = true
return renderFnWithContext
}
很简单,在创建slot的vnode之前,当前渲染的instance已经被赋值成了父组件的instance,在创建完之后还原。
作用域插槽实现原理
同样的:
<layout>
<template v-slot:header="slotScope">
<h1>{{slotScope.item}}</h1>
</template>
</layout>
...
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_layout = _resolveComponent("layout")
return (_openBlock(), _createBlock(_component_layout, null, {
header: _withCtx((slotScope) => [
_createVNode("h1", null, _toDisplayString(slotScope.item), 1 /* TEXT */)
]),
_: 1 /* STABLE */
}))
}
多了一个slotScope
的入参
再看一下layout
组件:
<div class="layout">
<header>
<slot name="header" :item="item"></slot>
</header>
</div>
import { renderSlot, createVNode, openBlock, createBlock } from "vue"
// Binding optimization for webpack code-split
const _renderSlot = renderSlot, _createVNode = createVNode, _openBlock = openBlock, _createBlock = createBlock
const _hoisted_1 = { class: "layout" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createVNode("header", null, [
_renderSlot(_ctx.$slots, "header", { item: _ctx.item })
])
]))
}
可以看到,_renderSlot
中多了一个{ item: _ctx.item }
,这时候_ctx
就是layout
组件的实例
总结
- slot的内容是在子组件渲染的时候才开始创建vnode节点的,然后渲染在子组件的对应节点中。
- 通过对创建slot内容的vnode函数通过withCtx包装,实现slot中访问的是父组件的作用域
作用域插槽原理
:因为子组件渲染的时候才会开始执行创建slot的vnode,所以在创建slot的vnode时,将子组件的实例作为参数传进去,则slot中可以访问到子组件作用域的数据