Skip to content

Commit

Permalink
fix: ensure scoped slots update in conditional branches
Browse files Browse the repository at this point in the history
close #9534
  • Loading branch information
yyx990803 committed Feb 21, 2019
1 parent b3bd311 commit d9b27a9
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 11 deletions.
33 changes: 28 additions & 5 deletions src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ function genScopedSlots (
containsSlotChild(slot) // is passing down slot from parent which may be dynamic
)
})

// #9534: if a component with scoped slots is inside a conditional branch,
// it's possible for the same component to be reused but with different
// compiled slot content. To avoid that, we generate a unique key based on
// the generated code of all the slot contents.
let needsKey = !!el.if

// OR when it is inside another scoped slot or v-for (the reactivity may be
// disconnected due to the intermediate scope variable)
// #9438, #9506
Expand All @@ -390,15 +397,31 @@ function genScopedSlots (
needsForceUpdate = true
break
}
if (parent.if) {
needsKey = true
}
parent = parent.parent
}
}

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

return `scopedSlots:_u([${generatedSlots}]${
needsForceUpdate ? `,true` : ``
}${
!needsForceUpdate && needsKey ? `,false,${hash(generatedSlots)}` : ``
})`
}

function hash(str) {
let hash = 5381
let i = str.length
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i)
}
return hash >>> 0
}

function containsSlotChild (el: ASTNode): boolean {
Expand Down
7 changes: 5 additions & 2 deletions src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,12 @@ export function updateChildComponent (
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(parentVnode.data.scopedSlots && !parentVnode.data.scopedSlots.$stable) ||
(vm.$scopedSlots !== emptyObject && !vm.$scopedSlots.$stable)
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
)

// Any static slot children from the parent may have changed during parent's
Expand Down
8 changes: 6 additions & 2 deletions src/core/instance/render-helpers/resolve-scoped-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
hasDynamicKeys?: boolean,
hasDynamicKeys: boolean,
contentHashKey: number,
res?: Object
): { [key: string]: Function, $stable: boolean } {
res = res || { $stable: !hasDynamicKeys }
for (let i = 0; i < fns.length; i++) {
const slot = fns[i]
if (Array.isArray(slot)) {
resolveScopedSlots(slot, hasDynamicKeys, res)
resolveScopedSlots(slot, hasDynamicKeys, null, res)
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
Expand All @@ -18,5 +19,8 @@ export function resolveScopedSlots (
res[slot.key] = slot.fn
}
}
if (contentHashKey) {
res.$key = contentHashKey
}
return res
}
8 changes: 6 additions & 2 deletions src/core/vdom/helpers/normalize-scoped-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ export function normalizeScopedSlots (
prevSlots?: { [key: string]: Function } | void
): any {
let res
const isStable = slots ? !!slots.$stable : true
const key = slots && slots.$key
if (!slots) {
res = {}
} else if (slots._normalized) {
// fast path 1: child component re-render only, parent did not change
return slots._normalized
} else if (
slots.$stable &&
isStable &&
prevSlots &&
prevSlots !== emptyObject &&
key === prevSlots.$key &&
Object.keys(normalSlots).length === 0
) {
// fast path 2: stable scoped slots w/ no normal slots to proxy,
Expand All @@ -43,7 +46,8 @@ export function normalizeScopedSlots (
if (slots && Object.isExtensible(slots)) {
(slots: any)._normalized = res
}
def(res, '$stable', slots ? !!slots.$stable : true)
def(res, '$stable', isStable)
def(res, '$key', key)
return res
}

Expand Down
30 changes: 30 additions & 0 deletions test/unit/features/component/component-scoped-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1197,4 +1197,34 @@ describe('Component scoped slot', () => {
expect(vm.$el.textContent).toBe(`2`)
}).then(done)
})

// #9534
it('should detect conditional reuse with different slot content', done => {
const Foo = {
template: `<div><slot :n="1" /></div>`
}

const vm = new Vue({
components: { Foo },
data: {
ok: true
},
template: `
<div>
<div v-if="ok">
<foo v-slot="{ n }">{{ n }}</foo>
</div>
<div v-if="!ok">
<foo v-slot="{ n }">{{ n + 1 }}</foo>
</div>
</div>
`
}).$mount()

expect(vm.$el.textContent.trim()).toBe(`1`)
vm.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent.trim()).toBe(`2`)
}).then(done)
})
})

0 comments on commit d9b27a9

Please sign in to comment.