Vue提供了pro可以進行參數(shù)的傳遞,但是有時需要給子組件的模板進行定制化,此時傳遞參數(shù)有時候就不太方便了。 Vue借鑒了Web Components實現(xiàn)了插槽slot。
插槽slot通過在父組件中編寫DOM,在子組件渲染的時候將這些DOM放置在對應的位置,從而實現(xiàn)內容的分發(fā)。
使用方法介紹
基本使用
<Son>
<p>父組件傳入的內容</p>
</Son>
我們想將一些內容渲染在Son子組件中,我們在組件中間寫了一些內容,例如<p>父組件傳入的內容</p>,但是最終這些內容會被Vue拋棄,是不會被渲染出來的。
如果我們想將<p>父組件傳入的內容</p>這部分內容在子組件中渲染,則需要使用slot了。
<!-- Son.vue -->
<div class="card">
<slot></slot>
</div>
我們只需要在
Son組件模板中加入<slot></slot>標簽,則<p>父組件傳入的內容</p>將替換<slot></slot>渲染
渲染的結果:
<div class="card">
<p>父組件傳入的內容</p>
</div>
默認內容
有些情況下,如果父組件不傳入內容,插槽需要顯示默認的內容。這時候只需要在<slot></slot>中放置默認的內容就行:
<!-- Son.vue -->
<div class="card">
<slot>子組件的默認內容</slot>
</div>
- 如果父組件不傳入插槽內容,則渲染為:
<div class="card">
子組件的默認內容
</div>
- 如果父組件傳入插槽內容
<p>父組件傳入的內容</p>,則渲染為:
<div class="card">
<p>父組件傳入的內容</p>
</div>
具名插槽
在有些情況下可能需要多個插槽進行內容的放置, 這時候就需要給插槽一個名字:
<!-- Son.vue -->
<div class="card">
<slot name="header"></slot>
<slot>子組件的默認內容</slot>
<slot name="footer"></slot>
</div>
我們的例子中有三個插槽,其中
header,footer,還有一個沒有給名字,其實它也是有名字的,不寫名字它的名字就是default, 等同于<slot name="default">子組件的默認內容</slot>。
這時候可以根據(jù)名稱對每個插槽放置不同的內容:
<Son>
<p>父組件的內容1</p>
<p>父組件的內容2</p>
<template v-slot:header> 外部傳入的header </template>
<template v-slot:footer> 外部傳入的footer </template>
</Son>
渲染內容如下:
<div class="card">
外部傳入的header
<p>父組件的內容1</p>
<p>父組件的內容2</p>
外部傳入的footer
</div>
v-slot:header包含的內容替換<slot name="header"></slot>;
v-slot:footer包含的內容替換<slot name="footer"></slot>;
其他所有內容都被當成v-slot:default替換<slot></slot>;
插槽作用域
插槽的內容使用到數(shù)據(jù),那這個數(shù)據(jù)來自于于父組件,而不是子組件:
- 父組件
<!-- parent.vue -->
<Son>
<p>插槽的name {{ name }}</p>
</Son>
setup() {
return {
name: ref("parent"),
}
},
- 子組件
<!-- son.vue -->
<div class="card">
<slot></slot>
</div>
setup() {
return {
name: ref("Chile"),
}
},
渲染結果:
<div class="card">
插槽的name: parent
</div>
作用域插槽
我們剛才提到插槽的數(shù)據(jù)的作用域是父組件,有時候插槽也需要使用來自于子組件的數(shù)據(jù),這時候可以使用作用域插槽。
- 將數(shù)據(jù)以
pro的形式傳遞
<slot :pro="name"></slot>
- 父組件接收
pro
<template v-slot:default="pro">
<p>插槽的name {{ pro.pro }}</p>
</template>
此時渲染的內容:
插槽的name: child
實現(xiàn)原理介紹
分析案例:
<!-- Parent.vue -->
<Son>
<p>插槽的name {{ name }}</p>
<template v-slot:header> <p>外部傳入的header</p> </template>
<template v-slot:footer> <p>外部傳入的footer</p> </template>
</Son>
<!-- Son.vue -->
<div class="card">
<slot name="header"></slot>
<slot>子組件的默認內容</slot>
<slot name="footer"></slot>
</div>
渲染函數(shù)分析
- parent
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "外部傳入的header", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "外部傳入的footer", -1 /* HOISTED */)
function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Son = _resolveComponent("Son")
return (_openBlock(), _createBlock(_component_Son, null, {
header: _withCtx(() => [
_hoisted_1
]),
footer: _withCtx(() => [
_hoisted_2
]),
default: _withCtx(() => [
_createElementVNode("p", null, "插槽的name " + _toDisplayString(name), 1 /* TEXT */)
]),
_: 1 /* STABLE */
}))
}
}
生成子組件的
VNode時傳了1個children對象, 這個對象有headr,footer,default屬性,這 3個屬性的值就是對應的DOM。
- son
function render(_ctx, _cache) {
with (_ctx) {
const { renderSlot: _renderSlot, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_renderSlot($slots, "header"),
_renderSlot($slots, "default", {}, () => [
_hoisted_2
]),
_renderSlot($slots, "footer")
]))
}
}
聯(lián)系這兩個渲染函數(shù)我們就可以大概有個猜測:子組件渲染的時候遇到
slot這個標簽,然后就找對應名字的children對應的渲染DOM的內容,進行渲染。即通過renderSlot會渲染headr,footer,default這三個插槽的內容。
withCtx的作用
export function withCtx(
fn: Function,
ctx: ComponentInternalInstance | null = currentRenderingInstance,
isNonScopedSlot?: boolean // __COMPAT__ only
) {
const renderFnWithContext: ContextualRenderFn = (...args: any[]) => {
const prevInstance = setCurrentRenderingInstance(ctx)
const res = fn(...args)
setCurrentRenderingInstance(prevInstance)
return res
}
return renderFnWithContext
}
withCtx的作用封裝 返回的函數(shù)為傳入的fn,重要的是保存當前的組件實例currentRenderingInstance,作為函數(shù)的作用域。
保存children到組件實例的slots
-
setupComponentsetup組件實例的時候會調用initSlots
setup組件實例是什么作用?如果不知道可以參閱我前面的文章。不想看,可以直接理解為先準備數(shù)據(jù)的階段,之后會進行組件渲染。
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
initSlots(instance, children)
}
-
children保存到instance.slots上
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
// we should avoid the proxy object polluting the slots of the internal instance
instance.slots = toRaw(children as InternalSlots)
def(instance.slots, "__vInternal", 1)
}
renderSlot渲染slot內容對應的VNode
export function renderSlot(
slots: Slots,
name: string,
props: Data = {},
// this is not a user-facing function, so the fallback is always generated by
// the compiler and guaranteed to be a function returning an array
fallback?: () => VNodeArrayChildren,
noSlotted?: boolean
): VNode {
let slot = slots[name]
const validSlotContent = slot && ensureValidVNode(slot(props))
const rendered = createBlock(
Fragment,
{ key: props.key || `_${name}` },
validSlotContent || (fallback ? fallback() : []),
validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL
)
return rendered
}
renderSlot創(chuàng)建的VNode是一個類型為Fragment,children為對應name的插槽的返回值。
結合前面的
withCtx的分析,總結來就是renderSlot創(chuàng)建的VNode是一個類型為Fragment,children為對應name的插槽的內容,但是插槽內的數(shù)據(jù)的作用域是屬于父組件的。
processFragment掛載slot內容對應的DOM
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
//
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (n1 == null) {
// 插入兩個空文本節(jié)點
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// 掛載數(shù)組子節(jié)點
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新數(shù)組子節(jié)點
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
processFragment先插入兩個空文本節(jié)點作為錨點,然后掛載數(shù)組子節(jié)點。
作用域插槽和默認內容的實現(xiàn)邏輯
// 默認內容
const _hoisted_2 = /*#__PURE__*/_createTextVNode("子組件的默認內容")
// pro
renderSlot($slots, "default", { pro: name }, () => [
_hoisted_2
])
子組件的數(shù)據(jù)和默認插槽內容作為
renderSlot函數(shù)的第3個和第4個參數(shù),進行插槽的內容渲染。
我們再回到 renderSlot函數(shù)
/**
* @param slots 組件VNode的slots
* @param name slot的name
* @param props slot的pro
* @param fallback 默認的內容
* @param noSlotted
* @returns
*/
export function renderSlot(
slots: Slots,
name: string,
props: Data = {},
fallback?: () => VNodeArrayChildren,
noSlotted?: boolean
): VNode {
// 從 組件VNode的slots對象中找到name對應的渲染函數(shù)
let slot = slots[name]
// props作為參數(shù)執(zhí)行渲染函數(shù),這樣渲染函數(shù)就拿到了子組件的數(shù)據(jù)
const validSlotContent = slot && ensureValidVNode(slot(props))
const rendered = createBlock(
Fragment,
{ key: props.key || `_${name}` },
validSlotContent || (fallback ? fallback() : []),
PatchFlags.STABLE_FRAGMENT
)
return rendered
}
renderSlot函數(shù)接收pros的參數(shù),將其傳入slots對象中找到name對應的渲染函數(shù),這樣就能獲取到子組件的數(shù)據(jù)pros了;
fallback是默認的渲染函數(shù),如果父組件沒有傳遞slot,就渲染默認的DOM。
總結
- 父組件渲染的時候生成一些
withCtx包含的渲染函數(shù),此時將父組件的實例對象持有在函數(shù)內部,,所以數(shù)據(jù)的作用域是父組件;- 子組件在
setupComponent先將這些withCtx包含的渲染函數(shù)存儲在子組件實例對象的slots上;- 子組件渲染的時候,插槽內容的渲染是先找到
slots中對應的withCtx包含的渲染函數(shù),然后傳入子組件的pro和默認的渲染DOM內容,最后生成插槽渲染內容的DOM內容。
一句話總結:父組件先編寫DOM存在子組件實例對象上,渲染子組件的時候再渲染對應的這部分DOM內容。