Skip to content

Commit 4d4d22a

Browse files
committed
fix: bail out scoped slot optimization when there are nested scopes
fix #9438
1 parent b6247fc commit 4d4d22a

File tree

3 files changed

+71
-7
lines changed

3 files changed

+71
-7
lines changed

src/compiler/codegen/index.js

+26-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { genHandlers } from './events'
44
import baseDirectives from '../directives/index'
55
import { camelize, no, extend } from 'shared/util'
66
import { baseWarn, pluckModuleFunction } from '../helpers'
7+
import { emptySlotScopeToken } from '../parser/index'
78

89
type TransformFunction = (el: ASTElement, code: string) => string;
910
type DataGenFunction = (el: ASTElement) => string;
@@ -268,7 +269,7 @@ export function genData (el: ASTElement, state: CodegenState): string {
268269
}
269270
// scoped slots
270271
if (el.scopedSlots) {
271-
data += `${genScopedSlots(el.scopedSlots, state)},`
272+
data += `${genScopedSlots(el, el.scopedSlots, state)},`
272273
}
273274
// component v-model
274275
if (el.model) {
@@ -357,18 +358,36 @@ function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {
357358
}
358359

359360
function genScopedSlots (
361+
el: ASTElement,
360362
slots: { [key: string]: ASTElement },
361363
state: CodegenState
362364
): string {
363-
const hasDynamicKeys = Object.keys(slots).some(key => {
365+
// by default scoped slots are considered "stable", this allows child
366+
// components with only scoped slots to skip forced updates from parent.
367+
// but in some cases we have to bail-out of this optimization
368+
// for example if the slot contains dynamic names, has v-if or v-for on them...
369+
let needsForceUpdate = Object.keys(slots).some(key => {
364370
const slot = slots[key]
365371
return slot.slotTargetDynamic || slot.if || slot.for
366372
})
373+
// OR when it is inside another scoped slot (the reactivity is disconnected)
374+
// #9438
375+
if (!needsForceUpdate) {
376+
let parent = el.parent
377+
while (parent) {
378+
if (parent.slotScope && parent.slotScope !== emptySlotScopeToken) {
379+
needsForceUpdate = true
380+
break
381+
}
382+
parent = parent.parent
383+
}
384+
}
385+
367386
return `scopedSlots:_u([${
368387
Object.keys(slots).map(key => {
369388
return genScopedSlot(slots[key], state)
370389
}).join(',')
371-
}]${hasDynamicKeys ? `,true` : ``})`
390+
}]${needsForceUpdate ? `,true` : ``})`
372391
}
373392

374393
function genScopedSlot (
@@ -382,7 +401,10 @@ function genScopedSlot (
382401
if (el.for && !el.forProcessed) {
383402
return genFor(el, state, genScopedSlot)
384403
}
385-
const fn = `function(${String(el.slotScope)}){` +
404+
const slotScope = el.slotScope === emptySlotScopeToken
405+
? ``
406+
: String(el.slotScope)
407+
const fn = `function(${slotScope}){` +
386408
`return ${el.tag === 'template'
387409
? el.if && isLegacySyntax
388410
? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`

src/compiler/parser/index.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const invalidAttributeRE = /[\s"'<>\/=]/
4444

4545
const decodeHTMLCached = cached(he.decode)
4646

47+
export const emptySlotScopeToken = `_empty_`
48+
4749
// configurable state
4850
export let warn: any
4951
let delimiters
@@ -659,7 +661,7 @@ function processSlotContent (el) {
659661
const { name, dynamic } = getSlotName(slotBinding)
660662
el.slotTarget = name
661663
el.slotTargetDynamic = dynamic
662-
el.slotScope = slotBinding.value || `_` // force it into a scoped slot for perf
664+
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
663665
}
664666
} else {
665667
// v-slot on component, denotes default slot
@@ -692,8 +694,13 @@ function processSlotContent (el) {
692694
const slotContainer = slots[name] = createASTElement('template', [], el)
693695
slotContainer.slotTarget = name
694696
slotContainer.slotTargetDynamic = dynamic
695-
slotContainer.children = el.children.filter(c => !(c: any).slotScope)
696-
slotContainer.slotScope = slotBinding.value || `_`
697+
slotContainer.children = el.children.filter((c: any) => {
698+
if (!c.slotScope) {
699+
c.parent = slotContainer
700+
return true
701+
}
702+
})
703+
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
697704
// remove children as they are returned from scopedSlots now
698705
el.children = []
699706
// mark el non-plain so data gets generated

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

+35
Original file line numberDiff line numberDiff line change
@@ -992,4 +992,39 @@ describe('Component scoped slot', () => {
992992
expect(Child.updated).not.toHaveBeenCalled()
993993
}).then(done)
994994
})
995+
996+
// regression #9438
997+
it('nested scoped slots update', done => {
998+
const Wrapper = {
999+
template: `<div><slot/></div>`
1000+
}
1001+
1002+
const Inner = {
1003+
props: ['foo'],
1004+
template: `<div>{{ foo }}</div>`
1005+
}
1006+
1007+
const Outer = {
1008+
data: () => ({ foo: 1 }),
1009+
template: `<div><slot :foo="foo" /></div>`
1010+
}
1011+
1012+
const vm = new Vue({
1013+
components: { Outer, Wrapper, Inner },
1014+
template: `
1015+
<outer ref="outer" v-slot="props">
1016+
<wrapper v-slot>
1017+
<inner :foo="props.foo"/>
1018+
</wrapper>
1019+
</outer>
1020+
`
1021+
}).$mount()
1022+
1023+
expect(vm.$el.textContent).toBe(`1`)
1024+
1025+
vm.$refs.outer.foo++
1026+
waitForUpdate(() => {
1027+
expect(vm.$el.textContent).toBe(`2`)
1028+
}).then(done)
1029+
})
9951030
})

0 commit comments

Comments
 (0)