vue3-core/packages/runtime-core/src/componentSlots.ts

257 lines
7.2 KiB
TypeScript

import { type ComponentInternalInstance, currentInstance } from './component'
import {
type VNode,
type VNodeChild,
type VNodeNormalizedChildren,
normalizeVNode,
} from './vnode'
import {
EMPTY_OBJ,
type IfAny,
type Prettify,
ShapeFlags,
SlotFlags,
def,
isArray,
isFunction,
} from '@vue/shared'
import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive'
import {
type ContextualRenderFn,
currentRenderingInstance,
withCtx,
} from './componentRenderContext'
import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
import { TriggerOpTypes, trigger } from '@vue/reactivity'
import { createInternalObject } from './internalObject'
export type Slot<T extends any = any> = (
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
) => VNode[]
export type InternalSlots = {
[name: string]: Slot | undefined
}
export type Slots = Readonly<InternalSlots>
declare const SlotSymbol: unique symbol
export type SlotsType<T extends Record<string, any> = Record<string, any>> = {
[SlotSymbol]?: T
}
export type StrictUnwrapSlotsType<
S extends SlotsType,
T = NonNullable<S[typeof SlotSymbol]>,
> = [keyof S] extends [never] ? Slots : Readonly<T> & T
export type UnwrapSlotsType<
S extends SlotsType,
T = NonNullable<S[typeof SlotSymbol]>,
> = [keyof S] extends [never]
? Slots
: Readonly<
Prettify<{
[K in keyof T]: NonNullable<T[K]> extends (...args: any[]) => any
? T[K]
: Slot<T[K]>
}>
>
export type RawSlots = {
[name: string]: unknown
// manual render fn hint to skip forced children updates
$stable?: boolean
/**
* for tracking slot owner instance. This is attached during
* normalizeChildren when the component vnode is created.
* @internal
*/
_ctx?: ComponentInternalInstance | null
/**
* indicates compiler generated slots
* we use a reserved property instead of a vnode patchFlag because the slots
* object may be directly passed down to a child component in a manual
* render function, and the optimization hint need to be on the slot object
* itself to be preserved.
* @internal
*/
_?: SlotFlags
/**
* cache indexes for slot content
* @internal
*/
__?: number[]
}
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
const normalizeSlotValue = (value: unknown): VNode[] =>
isArray(value)
? value.map(normalizeVNode)
: [normalizeVNode(value as VNodeChild)]
const normalizeSlot = (
key: string,
rawSlot: Function,
ctx: ComponentInternalInstance | null | undefined,
): Slot => {
if ((rawSlot as any)._n) {
// already normalized - #5353
return rawSlot as Slot
}
const normalized = withCtx((...args: any[]) => {
if (
__DEV__ &&
currentInstance &&
!(ctx === null && currentRenderingInstance) &&
!(ctx && ctx.root !== currentInstance.root)
) {
warn(
`Slot "${key}" invoked outside of the render function: ` +
`this will not track dependencies used in the slot. ` +
`Invoke the slot function inside the render function instead.`,
)
}
return normalizeSlotValue(rawSlot(...args))
}, ctx) as Slot
// NOT a compiled slot
;(normalized as ContextualRenderFn)._c = false
return 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)
} else if (value != null) {
if (
__DEV__ &&
!(
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)
)
) {
warn(
`Non-function value encountered for slot "${key}". ` +
`Prefer function slots for better performance.`,
)
}
const normalized = normalizeSlotValue(value)
slots[key] = () => normalized
}
}
}
const normalizeVNodeSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
) => {
if (
__DEV__ &&
!isKeepAlive(instance.vnode) &&
!(__COMPAT__ && isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance))
) {
warn(
`Non-function value encountered for default slot. ` +
`Prefer function slots for better performance.`,
)
}
const normalized = normalizeSlotValue(children)
instance.slots.default = () => normalized
}
const assignSlots = (
slots: InternalSlots,
children: Slots,
optimized: boolean,
) => {
for (const key in children) {
// #2893
// when rendering the optimized slots by manually written render function,
// do not copy the `slots._` compiler flag so that `renderSlot` creates
// slot Fragment with BAIL patchFlag to force full updates
if (optimized || !isInternalKey(key)) {
slots[key] = children[key]
}
}
}
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
optimized: boolean,
): void => {
const slots = (instance.slots = createInternalObject())
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
assignSlots(slots, children as Slots, optimized)
// make compiler marker non-enumerable
if (optimized) {
def(slots, '_', type, true)
}
} else {
normalizeObjectSlots(children as RawSlots, slots, instance)
}
} else if (children) {
normalizeVNodeSlots(instance, children)
}
}
export const updateSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
optimized: boolean,
): void => {
const { vnode, slots } = instance
let needDeletionCheck = true
let deletionComparisonTarget = EMPTY_OBJ
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
// compiled slots.
if (__DEV__ && isHmrUpdating) {
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
assignSlots(slots, children as Slots, optimized)
trigger(instance, TriggerOpTypes.SET, '$slots')
} else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
// no need to update, and skip stale slots removal.
needDeletionCheck = false
} else {
// compiled but dynamic (v-if/v-for on slots) - update slots, but skip
// normalization.
assignSlots(slots, children as Slots, optimized)
}
} else {
needDeletionCheck = !(children as RawSlots).$stable
normalizeObjectSlots(children as RawSlots, slots, instance)
}
deletionComparisonTarget = children as RawSlots
} else if (children) {
// non slot object children (direct value) passed to a component
normalizeVNodeSlots(instance, children)
deletionComparisonTarget = { default: 1 }
}
// delete stale slots
if (needDeletionCheck) {
for (const key in slots) {
if (!isInternalKey(key) && deletionComparisonTarget[key] == null) {
delete slots[key]
}
}
}
}