Skip to content

Commit d9b27a9

Browse files
committed
fix: ensure scoped slots update in conditional branches
close #9534
1 parent b3bd311 commit d9b27a9

File tree

5 files changed

+75
-11
lines changed

5 files changed

+75
-11
lines changed

src/compiler/codegen/index.js

+28-5
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,13 @@ function genScopedSlots (
375375
containsSlotChild(slot) // is passing down slot from parent which may be dynamic
376376
)
377377
})
378+
379+
// #9534: if a component with scoped slots is inside a conditional branch,
380+
// it's possible for the same component to be reused but with different
381+
// compiled slot content. To avoid that, we generate a unique key based on
382+
// the generated code of all the slot contents.
383+
let needsKey = !!el.if
384+
378385
// OR when it is inside another scoped slot or v-for (the reactivity may be
379386
// disconnected due to the intermediate scope variable)
380387
// #9438, #9506
@@ -390,15 +397,31 @@ function genScopedSlots (
390397
needsForceUpdate = true
391398
break
392399
}
400+
if (parent.if) {
401+
needsKey = true
402+
}
393403
parent = parent.parent
394404
}
395405
}
396406

397-
return `scopedSlots:_u([${
398-
Object.keys(slots).map(key => {
399-
return genScopedSlot(slots[key], state)
400-
}).join(',')
401-
}]${needsForceUpdate ? `,true` : ``})`
407+
const generatedSlots = Object.keys(slots)
408+
.map(key => genScopedSlot(slots[key], state))
409+
.join(',')
410+
411+
return `scopedSlots:_u([${generatedSlots}]${
412+
needsForceUpdate ? `,true` : ``
413+
}${
414+
!needsForceUpdate && needsKey ? `,false,${hash(generatedSlots)}` : ``
415+
})`
416+
}
417+
418+
function hash(str) {
419+
let hash = 5381
420+
let i = str.length
421+
while(i) {
422+
hash = (hash * 33) ^ str.charCodeAt(--i)
423+
}
424+
return hash >>> 0
402425
}
403426

404427
function containsSlotChild (el: ASTNode): boolean {

src/core/instance/lifecycle.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,12 @@ export function updateChildComponent (
229229
// check if there are dynamic scopedSlots (hand-written or compiled but with
230230
// dynamic slot names). Static scoped slots compiled from template has the
231231
// "$stable" marker.
232+
const newScopedSlots = parentVnode.data.scopedSlots
233+
const oldScopedSlots = vm.$scopedSlots
232234
const hasDynamicScopedSlot = !!(
233-
(parentVnode.data.scopedSlots && !parentVnode.data.scopedSlots.$stable) ||
234-
(vm.$scopedSlots !== emptyObject && !vm.$scopedSlots.$stable)
235+
(newScopedSlots && !newScopedSlots.$stable) ||
236+
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
237+
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
235238
)
236239

237240
// Any static slot children from the parent may have changed during parent's

src/core/instance/render-helpers/resolve-scoped-slots.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
export function resolveScopedSlots (
44
fns: ScopedSlotsData, // see flow/vnode
5-
hasDynamicKeys?: boolean,
5+
hasDynamicKeys: boolean,
6+
contentHashKey: number,
67
res?: Object
78
): { [key: string]: Function, $stable: boolean } {
89
res = res || { $stable: !hasDynamicKeys }
910
for (let i = 0; i < fns.length; i++) {
1011
const slot = fns[i]
1112
if (Array.isArray(slot)) {
12-
resolveScopedSlots(slot, hasDynamicKeys, res)
13+
resolveScopedSlots(slot, hasDynamicKeys, null, res)
1314
} else if (slot) {
1415
// marker for reverse proxying v-slot without scope on this.$slots
1516
if (slot.proxy) {
@@ -18,5 +19,8 @@ export function resolveScopedSlots (
1819
res[slot.key] = slot.fn
1920
}
2021
}
22+
if (contentHashKey) {
23+
res.$key = contentHashKey
24+
}
2125
return res
2226
}

src/core/vdom/helpers/normalize-scoped-slots.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ export function normalizeScopedSlots (
1010
prevSlots?: { [key: string]: Function } | void
1111
): any {
1212
let res
13+
const isStable = slots ? !!slots.$stable : true
14+
const key = slots && slots.$key
1315
if (!slots) {
1416
res = {}
1517
} else if (slots._normalized) {
1618
// fast path 1: child component re-render only, parent did not change
1719
return slots._normalized
1820
} else if (
19-
slots.$stable &&
21+
isStable &&
2022
prevSlots &&
2123
prevSlots !== emptyObject &&
24+
key === prevSlots.$key &&
2225
Object.keys(normalSlots).length === 0
2326
) {
2427
// fast path 2: stable scoped slots w/ no normal slots to proxy,
@@ -43,7 +46,8 @@ export function normalizeScopedSlots (
4346
if (slots && Object.isExtensible(slots)) {
4447
(slots: any)._normalized = res
4548
}
46-
def(res, '$stable', slots ? !!slots.$stable : true)
49+
def(res, '$stable', isStable)
50+
def(res, '$key', key)
4751
return res
4852
}
4953

test/unit/features/component/component-scoped-slot.spec.js

+30
Original file line numberDiff line numberDiff line change
@@ -1197,4 +1197,34 @@ describe('Component scoped slot', () => {
11971197
expect(vm.$el.textContent).toBe(`2`)
11981198
}).then(done)
11991199
})
1200+
1201+
// #9534
1202+
it('should detect conditional reuse with different slot content', done => {
1203+
const Foo = {
1204+
template: `<div><slot :n="1" /></div>`
1205+
}
1206+
1207+
const vm = new Vue({
1208+
components: { Foo },
1209+
data: {
1210+
ok: true
1211+
},
1212+
template: `
1213+
<div>
1214+
<div v-if="ok">
1215+
<foo v-slot="{ n }">{{ n }}</foo>
1216+
</div>
1217+
<div v-if="!ok">
1218+
<foo v-slot="{ n }">{{ n + 1 }}</foo>
1219+
</div>
1220+
</div>
1221+
`
1222+
}).$mount()
1223+
1224+
expect(vm.$el.textContent.trim()).toBe(`1`)
1225+
vm.ok = false
1226+
waitForUpdate(() => {
1227+
expect(vm.$el.textContent.trim()).toBe(`2`)
1228+
}).then(done)
1229+
})
12001230
})

0 commit comments

Comments
 (0)