From aab99abd28a5d17f2d1966678b0d334975d21877 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 15 Jul 2020 20:12:49 -0400 Subject: [PATCH] fix(slots): properly force update on forwarded slots fix #1594 --- .../__tests__/transforms/vSlot.spec.ts | 17 ++++++++++ .../compiler-core/src/transforms/vSlot.ts | 25 +++++++++++++- packages/runtime-core/src/componentSlots.ts | 34 ++++++++++--------- .../runtime-core/src/helpers/renderSlot.ts | 6 ++-- packages/runtime-core/src/vnode.ts | 17 ++++++++-- packages/shared/src/index.ts | 1 + packages/shared/src/slotFlags.ts | 21 ++++++++++++ 7 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 packages/shared/src/slotFlags.ts diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index c9a2e3e7cb9..0942b007df2 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -719,6 +719,23 @@ describe('compiler: transform component slots', () => { expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() }) + test('generate flag on forwarded slots', () => { + const { slots } = parseWithSlots(``) + expect(slots).toMatchObject({ + type: NodeTypes.JS_OBJECT_EXPRESSION, + properties: [ + { + key: { content: `default` }, + value: { type: NodeTypes.JS_FUNCTION_EXPRESSION } + }, + { + key: { content: `_` }, + value: { content: `3` } // forwarded + } + ] + }) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = jest.fn() diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index 0cec12745b1..fa8e471f674 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -33,6 +33,7 @@ import { } from '../utils' import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers' import { parseForExpression, createForLoopParams } from './vFor' +import { SlotFlags } from '@vue/shared/src' const defaultFallback = createSimpleExpression(`undefined`, false) @@ -321,13 +322,19 @@ export function buildSlots( } } + const slotFlag = hasDynamicSlots + ? SlotFlags.DYNAMIC + : hasForwardedSlots(node.children) + ? SlotFlags.FORWARDED + : SlotFlags.STABLE + let slots = createObjectExpression( slotsProperties.concat( createObjectProperty( `_`, // 2 = compiled but dynamic = can skip normalization, but must run diff // 1 = compiled and static = can skip normalization AND diff as optimized - createSimpleExpression(hasDynamicSlots ? `2` : `1`, false) + createSimpleExpression('' + slotFlag, false) ) ), loc @@ -354,3 +361,19 @@ function buildDynamicSlot( createObjectProperty(`fn`, fn) ]) } + +function hasForwardedSlots(children: TemplateChildNode[]): boolean { + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (child.type === NodeTypes.ELEMENT) { + if ( + child.tagType === ElementTypes.SLOT || + (child.tagType === ElementTypes.ELEMENT && + hasForwardedSlots(child.children)) + ) { + return true + } + } + } + return false +} diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index b0a52b0f505..4a6a1b75375 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -12,7 +12,8 @@ import { EMPTY_OBJ, ShapeFlags, extend, - def + def, + SlotFlags } from '@vue/shared' import { warn } from './warning' import { isKeepAlive } from './components/KeepAlive' @@ -27,24 +28,25 @@ export type InternalSlots = { export type Slots = Readonly -export const enum CompiledSlotTypes { - STATIC = 1, - DYNAMIC = 2 -} - 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. + /** + * for tracking slot owner instance. This is attached during + * normalizeChildren when the component vnode is created. + * @internal + */ _ctx?: ComponentInternalInstance | null - // internal, 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. - _?: CompiledSlotTypes + /** + * 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 } const isInternalKey = (key: string) => key[0] === '_' || key === '$stable' @@ -141,8 +143,8 @@ export const updateSlots = ( // Parent was HMR updated so slot content may have changed. // force update slots and mark instance for hmr as well extend(slots, children as Slots) - } else if (type === CompiledSlotTypes.STATIC) { - // compiled AND static. + } else if (type === SlotFlags.STABLE) { + // compiled AND stable. // no need to update, and skip stale slots removal. needDeletionCheck = false } else { diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 0cfb6ed694f..8744b4c3053 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -1,5 +1,5 @@ import { Data } from '../component' -import { Slots, RawSlots, CompiledSlotTypes } from '../componentSlots' +import { Slots, RawSlots } from '../componentSlots' import { VNodeArrayChildren, openBlock, @@ -7,7 +7,7 @@ import { Fragment, VNode } from '../vnode' -import { PatchFlags } from '@vue/shared' +import { PatchFlags, SlotFlags } from '@vue/shared' import { warn } from '../warning' /** @@ -39,7 +39,7 @@ export function renderSlot( Fragment, { key: props.key }, slot ? slot(props) : fallback ? fallback() : [], - (slots as RawSlots)._ === CompiledSlotTypes.STATIC + (slots as RawSlots)._ === SlotFlags.STABLE ? PatchFlags.STABLE_FRAGMENT : PatchFlags.BAIL ) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index aa87d440a04..a96519cb94c 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -8,7 +8,8 @@ import { normalizeClass, normalizeStyle, PatchFlags, - ShapeFlags + ShapeFlags, + SlotFlags } from '@vue/shared' import { ComponentInternalInstance, @@ -542,10 +543,22 @@ export function normalizeChildren(vnode: VNode, children: unknown) { return } else { type = ShapeFlags.SLOTS_CHILDREN - if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) { + const slotFlag = (children as RawSlots)._ + if (!slotFlag && !(InternalObjectKey in children!)) { // if slots are not normalized, attach context instance // (compiled / normalized slots already have context) ;(children as RawSlots)._ctx = currentRenderingInstance + } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) { + // a child component receives forwarded slots from the parent. + // its slot type is determined by its parent's slot type. + if ( + currentRenderingInstance.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS + ) { + ;(children as RawSlots)._ = SlotFlags.DYNAMIC + vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS + } else { + ;(children as RawSlots)._ = SlotFlags.STABLE + } } } } else if (isFunction(children)) { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 7caf7d78679..d886f074347 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,6 +3,7 @@ import { makeMap } from './makeMap' export { makeMap } export * from './patchFlags' export * from './shapeFlags' +export * from './slotFlags' export * from './globalsWhitelist' export * from './codeframe' export * from './mockWarn' diff --git a/packages/shared/src/slotFlags.ts b/packages/shared/src/slotFlags.ts new file mode 100644 index 00000000000..d111b5e6574 --- /dev/null +++ b/packages/shared/src/slotFlags.ts @@ -0,0 +1,21 @@ +export const enum SlotFlags { + /** + * Stable slots that only reference slot props or context state. The slot + * can fully capture its own dependencies so when passed down the parent won't + * need to force the child to update. + */ + STABLE = 1, + /** + * Slots that reference scope variables (v-for or an outer slot prop), or + * has conditional structure (v-if, v-for). The parent will need to force + * the child to update because the slot does not fully capture its dependencies. + */ + DYNAMIC = 2, + /** + * being forwarded into a child component. Whether the parent needs + * to update the child is dependent on what kind of slots the parent itself + * received. This has to be refined at runtime, when the child's vnode + * is being created (in `normalizeChildren`) + */ + FORWARDED = 3 +}