Skip to content

Commit

Permalink
fix: bail out scoped slot optimization when there are nested scopes
Browse files Browse the repository at this point in the history
fix #9438
  • Loading branch information
yyx990803 committed Feb 6, 2019
1 parent b6247fc commit 4d4d22a
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 7 deletions.
30 changes: 26 additions & 4 deletions src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { genHandlers } from './events'
import baseDirectives from '../directives/index'
import { camelize, no, extend } from 'shared/util'
import { baseWarn, pluckModuleFunction } from '../helpers'
import { emptySlotScopeToken } from '../parser/index'

type TransformFunction = (el: ASTElement, code: string) => string;
type DataGenFunction = (el: ASTElement) => string;
Expand Down Expand Up @@ -268,7 +269,7 @@ export function genData (el: ASTElement, state: CodegenState): string {
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el.scopedSlots, state)},`
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
Expand Down Expand Up @@ -357,18 +358,36 @@ function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {
}

function genScopedSlots (
el: ASTElement,
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
const hasDynamicKeys = Object.keys(slots).some(key => {
// by default scoped slots are considered "stable", this allows child
// components with only scoped slots to skip forced updates from parent.
// but in some cases we have to bail-out of this optimization
// for example if the slot contains dynamic names, has v-if or v-for on them...
let needsForceUpdate = Object.keys(slots).some(key => {
const slot = slots[key]
return slot.slotTargetDynamic || slot.if || slot.for
})
// OR when it is inside another scoped slot (the reactivity is disconnected)
// #9438
if (!needsForceUpdate) {
let parent = el.parent
while (parent) {
if (parent.slotScope && parent.slotScope !== emptySlotScopeToken) {
needsForceUpdate = true
break
}
parent = parent.parent
}
}

return `scopedSlots:_u([${
Object.keys(slots).map(key => {
return genScopedSlot(slots[key], state)
}).join(',')
}]${hasDynamicKeys ? `,true` : ``})`
}]${needsForceUpdate ? `,true` : ``})`
}

function genScopedSlot (
Expand All @@ -382,7 +401,10 @@ function genScopedSlot (
if (el.for && !el.forProcessed) {
return genFor(el, state, genScopedSlot)
}
const fn = `function(${String(el.slotScope)}){` +
const slotScope = el.slotScope === emptySlotScopeToken
? ``
: String(el.slotScope)
const fn = `function(${slotScope}){` +
`return ${el.tag === 'template'
? el.if && isLegacySyntax
? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
Expand Down
13 changes: 10 additions & 3 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const invalidAttributeRE = /[\s"'<>\/=]/

const decodeHTMLCached = cached(he.decode)

export const emptySlotScopeToken = `_empty_`

// configurable state
export let warn: any
let delimiters
Expand Down Expand Up @@ -659,7 +661,7 @@ function processSlotContent (el) {
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || `_` // force it into a scoped slot for perf
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
Expand Down Expand Up @@ -692,8 +694,13 @@ function processSlotContent (el) {
const slotContainer = slots[name] = createASTElement('template', [], el)
slotContainer.slotTarget = name
slotContainer.slotTargetDynamic = dynamic
slotContainer.children = el.children.filter(c => !(c: any).slotScope)
slotContainer.slotScope = slotBinding.value || `_`
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotScope) {
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
Expand Down
35 changes: 35 additions & 0 deletions test/unit/features/component/component-scoped-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,39 @@ describe('Component scoped slot', () => {
expect(Child.updated).not.toHaveBeenCalled()
}).then(done)
})

// regression #9438
it('nested scoped slots update', done => {
const Wrapper = {
template: `<div><slot/></div>`
}

const Inner = {
props: ['foo'],
template: `<div>{{ foo }}</div>`
}

const Outer = {
data: () => ({ foo: 1 }),
template: `<div><slot :foo="foo" /></div>`
}

const vm = new Vue({
components: { Outer, Wrapper, Inner },
template: `
<outer ref="outer" v-slot="props">
<wrapper v-slot>
<inner :foo="props.foo"/>
</wrapper>
</outer>
`
}).$mount()

expect(vm.$el.textContent).toBe(`1`)

vm.$refs.outer.foo++
waitForUpdate(() => {
expect(vm.$el.textContent).toBe(`2`)
}).then(done)
})
})

0 comments on commit 4d4d22a

Please sign in to comment.