fix(runtime-core): avoid manual slot invocation in template expressions interfering with block tracking

fix #1745
This commit is contained in:
Evan You 2020-08-06 10:16:13 -04:00
parent 233d191d0d
commit 791eff3dfb
4 changed files with 62 additions and 19 deletions

View File

@ -1,5 +1,13 @@
import { renderSlot } from '../../src/helpers/renderSlot' import { renderSlot } from '../../src/helpers/renderSlot'
import { h } from '../../src/h' import {
h,
withCtx,
createVNode,
openBlock,
createBlock,
Fragment
} from '../../src'
import { PatchFlags } from '@vue/shared/src'
describe('renderSlot', () => { describe('renderSlot', () => {
it('should render slot', () => { it('should render slot', () => {
@ -20,4 +28,23 @@ describe('renderSlot', () => {
renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default') renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default')
expect('SSR-optimized slot function detected').toHaveBeenWarned() expect('SSR-optimized slot function detected').toHaveBeenWarned()
}) })
// #1745
it('should force enable tracking', () => {
const slot = withCtx(
() => {
return [createVNode('div', null, 'foo', PatchFlags.TEXT)]
},
// mock instance
{} as any
)
// manual invocation should not track
const manual = (openBlock(), createBlock(Fragment, null, slot()))
expect(manual.dynamicChildren!.length).toBe(0)
// renderSlot should track
const templateRendered = renderSlot({ default: slot }, 'default')
expect(templateRendered.dynamicChildren!.length).toBe(1)
})
}) })

View File

@ -10,6 +10,8 @@ import {
import { PatchFlags, SlotFlags } from '@vue/shared' import { PatchFlags, SlotFlags } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
export let isRenderingTemplateSlot = false
/** /**
* Compiler runtime helper for rendering `<slot/>` * Compiler runtime helper for rendering `<slot/>`
* @private * @private
@ -33,15 +35,20 @@ export function renderSlot(
slot = () => [] slot = () => []
} }
return ( // a compiled slot disables block tracking by default to avoid manual
openBlock(), // invocation interfering with template-based block tracking, but in
createBlock( // `renderSlot` we can be sure that it's template-based so we can force
Fragment, // enable it.
{ key: props.key }, isRenderingTemplateSlot = true
slot ? slot(props) : fallback ? fallback() : [], const rendered = (openBlock(),
(slots as RawSlots)._ === SlotFlags.STABLE createBlock(
? PatchFlags.STABLE_FRAGMENT Fragment,
: PatchFlags.BAIL { key: props.key },
) slot ? slot(props) : fallback ? fallback() : [],
) (slots as RawSlots)._ === SlotFlags.STABLE
? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL
))
isRenderingTemplateSlot = false
return rendered
} }

View File

@ -4,6 +4,7 @@ import {
currentRenderingInstance currentRenderingInstance
} from '../componentRenderUtils' } from '../componentRenderUtils'
import { ComponentInternalInstance } from '../component' import { ComponentInternalInstance } from '../component'
import { setBlockTracking } from '../vnode'
/** /**
* Wrap a slot function to memoize current rendering instance * Wrap a slot function to memoize current rendering instance
@ -15,10 +16,15 @@ export function withCtx(
) { ) {
if (!ctx) return fn if (!ctx) return fn
return function renderFnWithContext() { return function renderFnWithContext() {
// By default, compiled slots disables block tracking since the user may
// call it inside a template expression (#1745). It should only track when
// it's called by a template `<slot>`.
setBlockTracking(-1)
const owner = currentRenderingInstance const owner = currentRenderingInstance
setCurrentRenderingInstance(ctx) setCurrentRenderingInstance(ctx)
const res = fn.apply(null, arguments as any) const res = fn.apply(null, arguments as any)
setCurrentRenderingInstance(owner) setCurrentRenderingInstance(owner)
setBlockTracking(1)
return res return res
} }
} }

View File

@ -35,6 +35,7 @@ import { currentRenderingInstance } from './componentRenderUtils'
import { RendererNode, RendererElement } from './renderer' import { RendererNode, RendererElement } from './renderer'
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
import { hmrDirtyComponents } from './hmr' import { hmrDirtyComponents } from './hmr'
import { isRenderingTemplateSlot } from './helpers/renderSlot'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true __isFragment: true
@ -400,18 +401,20 @@ function _createVNode(
normalizeChildren(vnode, children) normalizeChildren(vnode, children)
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
if ( if (
shouldTrack > 0 && (shouldTrack > 0 || isRenderingTemplateSlot) &&
// avoid a block node from tracking itself
!isBlockNode && !isBlockNode &&
// has current parent block
currentBlock && currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the // the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching. // vnode should not be considered dynamic due to handler caching.
patchFlag !== PatchFlags.HYDRATE_EVENTS && patchFlag !== PatchFlags.HYDRATE_EVENTS
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)
) { ) {
currentBlock.push(vnode) currentBlock.push(vnode)
} }