Skip to content

Commit 791eff3

Browse files
committed
fix(runtime-core): avoid manual slot invocation in template expressions interfering with block tracking
fix #1745
1 parent 233d191 commit 791eff3

File tree

4 files changed

+62
-19
lines changed

4 files changed

+62
-19
lines changed

packages/runtime-core/__tests__/helpers/renderSlot.spec.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { renderSlot } from '../../src/helpers/renderSlot'
2-
import { h } from '../../src/h'
2+
import {
3+
h,
4+
withCtx,
5+
createVNode,
6+
openBlock,
7+
createBlock,
8+
Fragment
9+
} from '../../src'
10+
import { PatchFlags } from '@vue/shared/src'
311

412
describe('renderSlot', () => {
513
it('should render slot', () => {
@@ -20,4 +28,23 @@ describe('renderSlot', () => {
2028
renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default')
2129
expect('SSR-optimized slot function detected').toHaveBeenWarned()
2230
})
31+
32+
// #1745
33+
it('should force enable tracking', () => {
34+
const slot = withCtx(
35+
() => {
36+
return [createVNode('div', null, 'foo', PatchFlags.TEXT)]
37+
},
38+
// mock instance
39+
{} as any
40+
)
41+
42+
// manual invocation should not track
43+
const manual = (openBlock(), createBlock(Fragment, null, slot()))
44+
expect(manual.dynamicChildren!.length).toBe(0)
45+
46+
// renderSlot should track
47+
const templateRendered = renderSlot({ default: slot }, 'default')
48+
expect(templateRendered.dynamicChildren!.length).toBe(1)
49+
})
2350
})

packages/runtime-core/src/helpers/renderSlot.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { PatchFlags, SlotFlags } from '@vue/shared'
1111
import { warn } from '../warning'
1212

13+
export let isRenderingTemplateSlot = false
14+
1315
/**
1416
* Compiler runtime helper for rendering `<slot/>`
1517
* @private
@@ -33,15 +35,20 @@ export function renderSlot(
3335
slot = () => []
3436
}
3537

36-
return (
37-
openBlock(),
38-
createBlock(
39-
Fragment,
40-
{ key: props.key },
41-
slot ? slot(props) : fallback ? fallback() : [],
42-
(slots as RawSlots)._ === SlotFlags.STABLE
43-
? PatchFlags.STABLE_FRAGMENT
44-
: PatchFlags.BAIL
45-
)
46-
)
38+
// a compiled slot disables block tracking by default to avoid manual
39+
// invocation interfering with template-based block tracking, but in
40+
// `renderSlot` we can be sure that it's template-based so we can force
41+
// enable it.
42+
isRenderingTemplateSlot = true
43+
const rendered = (openBlock(),
44+
createBlock(
45+
Fragment,
46+
{ key: props.key },
47+
slot ? slot(props) : fallback ? fallback() : [],
48+
(slots as RawSlots)._ === SlotFlags.STABLE
49+
? PatchFlags.STABLE_FRAGMENT
50+
: PatchFlags.BAIL
51+
))
52+
isRenderingTemplateSlot = false
53+
return rendered
4754
}

packages/runtime-core/src/helpers/withRenderContext.ts

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
currentRenderingInstance
55
} from '../componentRenderUtils'
66
import { ComponentInternalInstance } from '../component'
7+
import { setBlockTracking } from '../vnode'
78

89
/**
910
* Wrap a slot function to memoize current rendering instance
@@ -15,10 +16,15 @@ export function withCtx(
1516
) {
1617
if (!ctx) return fn
1718
return function renderFnWithContext() {
19+
// By default, compiled slots disables block tracking since the user may
20+
// call it inside a template expression (#1745). It should only track when
21+
// it's called by a template `<slot>`.
22+
setBlockTracking(-1)
1823
const owner = currentRenderingInstance
1924
setCurrentRenderingInstance(ctx)
2025
const res = fn.apply(null, arguments as any)
2126
setCurrentRenderingInstance(owner)
27+
setBlockTracking(1)
2228
return res
2329
}
2430
}

packages/runtime-core/src/vnode.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { currentRenderingInstance } from './componentRenderUtils'
3535
import { RendererNode, RendererElement } from './renderer'
3636
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
3737
import { hmrDirtyComponents } from './hmr'
38+
import { isRenderingTemplateSlot } from './helpers/renderSlot'
3839

3940
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
4041
__isFragment: true
@@ -400,18 +401,20 @@ function _createVNode(
400401

401402
normalizeChildren(vnode, children)
402403

403-
// presence of a patch flag indicates this node needs patching on updates.
404-
// component nodes also should always be patched, because even if the
405-
// component doesn't need to update, it needs to persist the instance on to
406-
// the next vnode so that it can be properly unmounted later.
407404
if (
408-
shouldTrack > 0 &&
405+
(shouldTrack > 0 || isRenderingTemplateSlot) &&
406+
// avoid a block node from tracking itself
409407
!isBlockNode &&
408+
// has current parent block
410409
currentBlock &&
410+
// presence of a patch flag indicates this node needs patching on updates.
411+
// component nodes also should always be patched, because even if the
412+
// component doesn't need to update, it needs to persist the instance on to
413+
// the next vnode so that it can be properly unmounted later.
414+
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
411415
// the EVENTS flag is only for hydration and if it is the only flag, the
412416
// vnode should not be considered dynamic due to handler caching.
413-
patchFlag !== PatchFlags.HYDRATE_EVENTS &&
414-
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)
417+
patchFlag !== PatchFlags.HYDRATE_EVENTS
415418
) {
416419
currentBlock.push(vnode)
417420
}

0 commit comments

Comments
 (0)