diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index ccba387d52b..0e619b709c9 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -494,6 +494,9 @@ const SetupProxyHandlers: { [key: string]: ProxyHandler } = {} if (__DEV__) { markAttrsAccessed() } + // if the user pass the slots proxy to h(), normalizeChildren should not + // attempt to attach ctx to the object + if (key === '_') return 1 return instance[type][key] }, has: (instance, key) => key === SetupProxySymbol || key in instance[type], diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 2958903e152..db049246e7c 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -8,6 +8,7 @@ import { import { isArray, isFunction, EMPTY_OBJ, ShapeFlags } from '@vue/shared' import { warn } from './warning' import { isKeepAlive } from './components/KeepAlive' +import { withCtx } from './helpers/withRenderContext' export type Slot = (...args: any[]) => VNode[] @@ -21,6 +22,9 @@ export type RawSlots = { [name: string]: unknown // manual render fn hint to skip forced children updates $stable?: boolean + // internal, for tracking slot owner instance. This is attached during + // normalizeChildren when the component vnode is created. + _ctx?: ComponentInternalInstance | null // internal, indicates compiler generated slots = can skip normalization _?: 1 } @@ -30,18 +34,21 @@ const normalizeSlotValue = (value: unknown): VNode[] => ? value.map(normalizeVNode) : [normalizeVNode(value as VNodeChild)] -const normalizeSlot = (key: string, rawSlot: Function): Slot => ( - props: any -) => { - if (__DEV__ && currentInstance != null) { - 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(props)) -} +const normalizeSlot = ( + key: string, + rawSlot: Function, + ctx: ComponentInternalInstance | null | undefined +): Slot => + withCtx((props: any) => { + if (__DEV__ && currentInstance != null) { + 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(props)) + }, ctx) export function resolveSlots( instance: ComponentInternalInstance, @@ -55,11 +62,12 @@ export function resolveSlots( slots = children as Slots } else { slots = {} + const ctx = rawSlots._ctx for (const key in rawSlots) { - if (key === '$stable') continue + if (key === '$stable' || key === '_ctx') continue const value = rawSlots[key] if (isFunction(value)) { - slots[key] = normalizeSlot(key, value) + slots[key] = normalizeSlot(key, value, ctx) } else if (value != null) { if (__DEV__) { warn( diff --git a/packages/runtime-core/src/helpers/scopeId.ts b/packages/runtime-core/src/helpers/scopeId.ts index a8ee24016df..0cc88046f32 100644 --- a/packages/runtime-core/src/helpers/scopeId.ts +++ b/packages/runtime-core/src/helpers/scopeId.ts @@ -24,13 +24,12 @@ export function popScopeId() { export function withScopeId(id: string): (fn: T) => T { if (__BUNDLER__) { return ((fn: Function, ctx?: ComponentInternalInstance) => { - function renderWithId(this: any) { + return withCtx(function(this: any) { pushScopeId(id) const res = fn.apply(this, arguments) popScopeId() return res - } - return ctx ? withCtx(renderWithId, ctx) : renderWithId + }, ctx) }) as any } else { return undefined as any diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts index 47ed8091d41..b3642a1f840 100644 --- a/packages/runtime-core/src/helpers/withRenderContext.ts +++ b/packages/runtime-core/src/helpers/withRenderContext.ts @@ -5,7 +5,11 @@ import { currentRenderingInstance } from '../componentRenderUtils' -export function withCtx(fn: Slot, ctx: ComponentInternalInstance) { +export function withCtx( + fn: Slot, + ctx: ComponentInternalInstance | null | undefined +) { + if (!ctx) return fn return function renderFnWithContext() { const owner = currentRenderingInstance setCurrentRenderingInstance(ctx) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 272731e8a05..ce91483c8cb 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -30,6 +30,7 @@ import { TransitionHooks } from './components/BaseTransition' import { warn } from './warning' import { currentScopeId } from './helpers/scopeId' import { PortalImpl, isPortal } from './components/Portal' +import { currentRenderingInstance } from './componentRenderUtils' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true @@ -387,8 +388,11 @@ export function normalizeChildren(vnode: VNode, children: unknown) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'object') { type = ShapeFlags.SLOTS_CHILDREN + if (!(children as RawSlots)._) { + ;(children as RawSlots)._ctx = currentRenderingInstance + } } else if (isFunction(children)) { - children = { default: children } + children = { default: children, _ctx: currentRenderingInstance } type = ShapeFlags.SLOTS_CHILDREN } else { children = String(children)