fix(slots): properly force update on forwarded slots

fix #1594
This commit is contained in:
Evan You 2020-07-15 20:12:49 -04:00
parent 44e6da1402
commit aab99abd28
7 changed files with 99 additions and 22 deletions

View File

@ -719,6 +719,23 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
}) })
test('generate flag on forwarded slots', () => {
const { slots } = parseWithSlots(`<Comp><slot/></Comp>`)
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', () => { describe('errors', () => {
test('error on extraneous children w/ named default slot', () => { test('error on extraneous children w/ named default slot', () => {
const onError = jest.fn() const onError = jest.fn()

View File

@ -33,6 +33,7 @@ import {
} from '../utils' } from '../utils'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers' import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor' import { parseForExpression, createForLoopParams } from './vFor'
import { SlotFlags } from '@vue/shared/src'
const defaultFallback = createSimpleExpression(`undefined`, false) 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( let slots = createObjectExpression(
slotsProperties.concat( slotsProperties.concat(
createObjectProperty( createObjectProperty(
`_`, `_`,
// 2 = compiled but dynamic = can skip normalization, but must run diff // 2 = compiled but dynamic = can skip normalization, but must run diff
// 1 = compiled and static = can skip normalization AND diff as optimized // 1 = compiled and static = can skip normalization AND diff as optimized
createSimpleExpression(hasDynamicSlots ? `2` : `1`, false) createSimpleExpression('' + slotFlag, false)
) )
), ),
loc loc
@ -354,3 +361,19 @@ function buildDynamicSlot(
createObjectProperty(`fn`, fn) 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
}

View File

@ -12,7 +12,8 @@ import {
EMPTY_OBJ, EMPTY_OBJ,
ShapeFlags, ShapeFlags,
extend, extend,
def def,
SlotFlags
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive' import { isKeepAlive } from './components/KeepAlive'
@ -27,24 +28,25 @@ export type InternalSlots = {
export type Slots = Readonly<InternalSlots> export type Slots = Readonly<InternalSlots>
export const enum CompiledSlotTypes {
STATIC = 1,
DYNAMIC = 2
}
export type RawSlots = { export type RawSlots = {
[name: string]: unknown [name: string]: unknown
// manual render fn hint to skip forced children updates // manual render fn hint to skip forced children updates
$stable?: boolean $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 _ctx?: ComponentInternalInstance | null
// internal, indicates compiler generated slots /**
// we use a reserved property instead of a vnode patchFlag because the slots * indicates compiler generated slots
// object may be directly passed down to a child component in a manual * we use a reserved property instead of a vnode patchFlag because the slots
// render function, and the optimization hint need to be on the slot object * object may be directly passed down to a child component in a manual
// itself to be preserved. * render function, and the optimization hint need to be on the slot object
_?: CompiledSlotTypes * itself to be preserved.
* @internal
*/
_?: SlotFlags
} }
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable' 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. // Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well // force update slots and mark instance for hmr as well
extend(slots, children as Slots) extend(slots, children as Slots)
} else if (type === CompiledSlotTypes.STATIC) { } else if (type === SlotFlags.STABLE) {
// compiled AND static. // compiled AND stable.
// no need to update, and skip stale slots removal. // no need to update, and skip stale slots removal.
needDeletionCheck = false needDeletionCheck = false
} else { } else {

View File

@ -1,5 +1,5 @@
import { Data } from '../component' import { Data } from '../component'
import { Slots, RawSlots, CompiledSlotTypes } from '../componentSlots' import { Slots, RawSlots } from '../componentSlots'
import { import {
VNodeArrayChildren, VNodeArrayChildren,
openBlock, openBlock,
@ -7,7 +7,7 @@ import {
Fragment, Fragment,
VNode VNode
} from '../vnode' } from '../vnode'
import { PatchFlags } from '@vue/shared' import { PatchFlags, SlotFlags } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
/** /**
@ -39,7 +39,7 @@ export function renderSlot(
Fragment, Fragment,
{ key: props.key }, { key: props.key },
slot ? slot(props) : fallback ? fallback() : [], slot ? slot(props) : fallback ? fallback() : [],
(slots as RawSlots)._ === CompiledSlotTypes.STATIC (slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT ? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL : PatchFlags.BAIL
) )

View File

@ -8,7 +8,8 @@ import {
normalizeClass, normalizeClass,
normalizeStyle, normalizeStyle,
PatchFlags, PatchFlags,
ShapeFlags ShapeFlags,
SlotFlags
} from '@vue/shared' } from '@vue/shared'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -542,10 +543,22 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
return return
} else { } else {
type = ShapeFlags.SLOTS_CHILDREN 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 // if slots are not normalized, attach context instance
// (compiled / normalized slots already have context) // (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance ;(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)) { } else if (isFunction(children)) {

View File

@ -3,6 +3,7 @@ import { makeMap } from './makeMap'
export { makeMap } export { makeMap }
export * from './patchFlags' export * from './patchFlags'
export * from './shapeFlags' export * from './shapeFlags'
export * from './slotFlags'
export * from './globalsWhitelist' export * from './globalsWhitelist'
export * from './codeframe' export * from './codeframe'
export * from './mockWarn' export * from './mockWarn'

View File

@ -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,
/**
* <slot/> 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
}