我们知道,在vue中可通过slot
可以传递外部传入的元素,比如这样
<template>
<div>
<slot />
</div>
</template>
当传递元素时,就可以这样使用
<template>
<Child>123</Child>
</template>
如果不考虑sfc的编译优化,可以将这上述组件转换为对应的h函数版本版本, 这里我们使用了单测来表示,并且通过断点调试这个单测,就可以知道slot的原理了
test('slot', async () => {
// 这个对应Child.vue
const Child = {
setup(props: any, { slots }: any) {
return () => h('div', null, slots.default())
},
}
// 对应main.js的createApp(App).mount('#app')
render(
// 这个对应App.vue
h(Child, null, { default: () => h('div', null, 123) }),
nodeOps.createElement('div')
)
})
通过调试可以知道在创建Child
这个组件的过程中,会先执行createBaseVNode
,在这个函数中会对第三个参数{ default: () => h('div', null, 123) }
进行处理
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
// 省略不相干代码
if (typeof children === 'object') {
// 标示为SLOTS_CHILDREN
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children as RawSlots)._
if (!slotFlag && !isInternalObject(children)) {
;(children as RawSlots)._ctx = currentRenderingInstance
}
}
// 省略不相干代码
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
然后就通过mountComponent
挂载Child组件,在mountComponent
函数中,又通过setupComponent
初始化,
这里我们只考虑initSlots
const mountComponent: MountComponentFn = (...) => {
// 省略不相干代码
setupComponent(instance)
// 省略不相干代码
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized
)
}
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
// 省略不相干代码
// 这里我们只考虑`initSlots`
initSlots(instance, children)
// 省略不相干代码
}
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
const slots = (instance.slots = createInternalObject())
// normalizeChildren 已经标识为 SLOTS_CHILDREN
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
// { default: () => h('div', null, 123) } 这个对象并没有 `_` 属性
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)
}
}
}
在initSlot
这个函数,我们知道会对children{ default: () => h('div', null, 123) }
使用normalizeObjectSlots和normalizeSlot
处理,
通过initSlot处理后,这个组件实例instance.slot就添加上了一个属性default
值为normalized
函数
const normalizeObjectSlots = (
rawSlots: RawSlots,
slots: InternalSlots,
instance: ComponentInternalInstance
) => {
const ctx = rawSlots._ctx
for (const key in rawSlots) {
if (isInternalKey(key)) continue
const value = rawSlots[key]
if (isFunction(value)) {
slots[key] = normalizeSlot(key, value, ctx)
}
// 省略不相干代码
}
}
const normalizeSlot = (
key: string,
rawSlot: Function,
ctx: ComponentInternalInstance | null | undefined
): Slot => {
const normalized = withCtx((...args: any[]) => {
return normalizeSlotValue(rawSlot(...args))
}, ctx) as Slot
return normalized
}
接下来在setupRenderEffect
函数中执行Child的setup返回的渲染函数() => h('div', null, slots.default())
,
现在已经通过initSlot
后,slots也已经存放了default函数() => h('div', null, 123)
,所以在Child组件中,
就接收到了外部出来的元素了
const setupRenderEffect: SetupRenderEffectFn = (...) => {
const componentUpdateFn = () => {
// 省略不相干代码
// 这里就会执行Child组件setup返回的render函数:() => h('div', null, slots.default())
instance.subTree = renderComponentRoot(instance)
// 省略不相干代码
}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
NOOP,
() => queueJob(update),
instance.scope, // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => {
if (effect.dirty) {
effect.run()
}
})
update()
}