问题
现在有组件 A,B,C,D 。 A嵌套B , B嵌套C,C嵌套D。 D定义了一个myslot插槽,并且传入参数msg 希望在A组件直接通过具名myslot插入D的内容。并且可以拿到msg的信息。
方法1 组件template转发
通过转发的方式,在B,和C ,都定义相同的命名插槽,并且的作用域同步传入。
A组件
<template>
<B>
<template #mysolt="{msg}">
{{msg}}
</template>
</B>
</template>
B组件
<template>
<C>
<template #mysolt="{msg}">
<slot name="mysolt" :msg="msg">
</slot>
</template>
</C>
</template>
C组件
<template>
<D>
<template #mysolt="{msg}">
<slot name="mysolt" :msg="msg">
</slot>
</template>
</D>
</template>
D组件
<template>
<div>
<slot name="mysolt" msg="我是D组件msg"> </slot>
</div>
</template>
缺点需要重复定义,改动大
方法2 CreateVNode + ctx.slots 传递
如果组件是通过CreateVNode创建,可以直接通过获取当前的slots信息 ,并且直接传给下一个CreateVNode里面的slots
组件A , 定义了插槽
<template>
<B>
<template #mysolt="{msg}">
{{msg}}
</template >
</B>
</template>
// 等价于 下面 defineComponent定义的写法
<script lang='ts'>
import B from './B.vue';
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({
setup(props, ctx) {
return () => createVNode(B, props,{ 'myslot' : (msg:string) => {return createVNode('p',null,msg) }} )
},
})
</script>
组件B , 引用了A定义的插槽
<script lang='ts'>
import C from './C.vue';
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({
setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
//等价于 ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }}
return () => createVNode(C, props,ctx.slots)
},
})
</script>
组件C, 还是引用了A定义的插槽
<script lang='ts'>
import D from './D.vue';
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({ //ctx.slots 包含了还是B的插槽信息,用的是上面Aslots
setup(props, ctx) {
return () => createVNode(D, props,ctx.slots)
},
})
</script>
组件D ,最后D插槽直接消费 A组件定义的插槽
<template>
<div>
<slot name="mysolt" msg="我是D组件msg">
</slot>
</div>
</template>
//等价于下面的defineComponent 的写法
<script lang='ts'>
import { defineComponent ,createVNode} from 'vue'
export default defineComponent({ //ctx.slots 包含了还是B的插槽信息,用的是上面Aslots
setup(props, ctx) {
let msg = '我是D组件的msg'
return () => createVNode('div', props,ctx.slots.myslot('msg'))
},
})
</script>
源码分析1
我们写一个 A 直接调用D的 demo
<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="d1">
<h1>D组件</h1>
<div>
<slot name="mysolt" msg="我是D组件msg"> </slot>
</div>
</script>
<script>
const D = {
template: '#d1',
}
</script>
<script type="text/x-template" id="a1">
<h1>A组件</h1>
<D>
<template #mysolt="{msg}">
{{msg}}
</template >
</D>
</script>
<script>
const A1 = {
template: '#a1',
components: {
D
},
setup(props) {
}
}
</script>
<div id="demo">
<A1>
</A1>
</div>
<script>
debugger
const app = Vue.createApp({
components: {
A1
},
setup() {
}
})
app.mount('#demo')
</script>
<style>
</style>
initSlots
在创建组件A的时候会调用 initSlots 方法,把插槽函数存储到当前实例的slots上面。
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
) => {
const slots = (instance.slots = createInternalObject()) // 在当前实例创建一个slots对象。里面记录所有插槽信息,
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
extend(slots, children as InternalSlots) // 这里开始记录
// make compiler marker non-enumerable
def(slots, '_', type, true)
} else {
normalizeObjectSlots(children as RawSlots, slots, instance)
}
} else if (children) {
normalizeVNodeSlots(instance, children)
}
}
renderSlot
通过调用栈,可以看到在创建A里面的D组件时候,使用 _renderSlot($slots, "mysolt", { msg: "我是D组件msg" }) 创建 D组件,其中 $slots是上面保存的组件实例里面的一个map, 里面包含了 A里面定义的插槽信息是一个函数,mysolt = () => {}
packages/runtime-core/src/helpers/renderSlot.ts
import type { Data } from '../component'
import type { RawSlots, Slots } from '../componentSlots'
import {
type ContextualRenderFn,
currentRenderingInstance,
} from '../componentRenderContext'
import {
Comment,
Fragment,
type VNode,
type VNodeArrayChildren,
createBlock,
isVNode,
openBlock,
} from '../vnode'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { warn } from '../warning'
import { createVNode } from '@vue/runtime-core'
import { isAsyncWrapper } from '../apiAsyncComponent'
/**
* Compiler runtime helper for rendering `<slot/>`
* @private
*/
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 {
if (
currentRenderingInstance!.isCE ||
(currentRenderingInstance!.parent &&
isAsyncWrapper(currentRenderingInstance!.parent) &&
currentRenderingInstance!.parent.isCE)
) {
if (name !== 'default') props.name = name
return createVNode('slot', props, fallback && fallback())
}
let slot = slots[name] // 这里 slots 缓存所有插槽的map
// a compiled slot disables block tracking by default to avoid manual
// invocation interfering with template-based block tracking, but in
// `renderSlot` we can be sure that it's template-based so we can force
// enable it.
if (slot && (slot as ContextualRenderFn)._c) {
;(slot as ContextualRenderFn)._d = false
}
openBlock()
const validSlotContent = slot && ensureValidVNode(slot(props)) // 这里调用插槽方法
const rendered = createBlock(
Fragment,
{
key:
props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`,
},
validSlotContent || (fallback ? fallback() : []),
validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL,
)
if (!noSlotted && rendered.scopeId) {
rendered.slotScopeIds = [rendered.scopeId + '-s']
}
if (slot && (slot as ContextualRenderFn)._c) {
;(slot as ContextualRenderFn)._d = true
}
return rendered // 返回一个渲染函数
}
function ensureValidVNode(vnodes: VNodeArrayChildren) {
return vnodes.some(child => {
if (!isVNode(child)) return true
if (child.type === Comment) return false
if (
child.type === Fragment &&
!ensureValidVNode(child.children as VNodeArrayChildren)
)
return false
return true
})
? vnodes
: null
}
- renderSlot方法通过前面保存的 slots 找到 对应的插槽方法
- 使用 slot && ensureValidVNode(slot(props)) 调用该方法,并且返回内容
- 通过createBlock(Fragment,{...},validSlotContent) 创建新的vnode 实现内容的输出
源码分析2
多个组件传递
<script src="../../dist/vue.global.js"></script>
<script type="text/x-template" id="d1">
<h1>D组件</h1>
<div>
<slot name="mysolt" msg="我是D组件msg"> </slot>
</div>
</script>
<script>
const D = {
template: '#d1',
}
</script>
<script type="text/x-template" id="c1">
<h1>C组件</h1>
<D>
<template #mysolt="{msg}">
<slot name="mysolt" :msg="msg">
</slot>
</template >
</D>
</script>
<script>
const C = {
template: '#c1',
components: {
D
}
}
</script>
<script type="text/x-template" id="b1">
<h1>B组件</h1>
<C>
<template #mysolt="{msg}">
<slot name="mysolt" :msg="msg">
</slot>
</template >
</C>
</script>
<script>
const B1 = {
template: '#b1',
components: {
C
}
}
</script>
<script type="text/x-template" id="a1">
<h1>A组件</h1>
<B1>
<template #mysolt="{msg}">
{{msg}}
</template >
</B1>
</script>
<script>
const { reactive, computed,ref,h } = Vue
const A1 = {
template: '#a1',
components: {
B1
},
setup(props) {
}
}
</script>
<div id="demo">
<A1>
</A1>
</div>
<script>
debugger
const app = Vue.createApp({
components: {
A1
},
setup() {
}
})
app.mount('#demo')
</script>
<style>
</style>
可以看到renderSlot 触发时机是在 D组件需要渲染时候,依此从D往B 执行renderSlot
- D组件 执行renderSlot
- C组件 执行renderSlot
- B组件 执行renderSlot
第一次 D组件
第二次 C组件
第三次 B组件
源码分析3
<script src="../../dist/vue.global.js"></script>
<div id="demo">
<A1>
</A1>
</div>
<script>
const { defineComponent,h ,createVNode} = Vue
// 定义 D 组件
const D = defineComponent({
template: `
<h1>D组件</h1>
<div>
<slot name="mysolt" msg="我是D组件msg"></slot>
</div>
`
});
// 定义 C 组件
const C = defineComponent({
components: { D },
setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
//等价于 ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }}
return () => createVNode(D, props,ctx.slots)
},
});
// 定义 B1 组件
const B1 = defineComponent({
components: { C },
setup(props, ctx) { //ctx.slots 包含了当前组件A的插槽信息
//等价于 ctx.slots = { 'myslot' : (msg:string) => {return createVNode('p',null,msg) }}
return () => createVNode(C, props,ctx.slots)
},
});
// 定义 A1 组件
const A1 = defineComponent({
components: { B1 },
template: `
<h1>A组件</h1>
<B1>
<template #mysolt="{ msg }">
{{ msg }}
</template>
</B1>
`
});
debugger
// 创建 Vue 应用并挂载
const { createApp } = Vue
const app = createApp({
components: { A1 }
});
app.mount('#demo');
</script>
可以看到中间的B 和 C组件使用 defineComponent定义 ctx.slots 透传的效果和 源码2 效果一致