From d9b27a92bd5277ee23a4e68a8bd31ecc72f4c99b Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 20 Feb 2019 21:48:33 -0500 Subject: [PATCH] fix: ensure scoped slots update in conditional branches close #9534 --- src/compiler/codegen/index.js | 33 ++++++++++++++++--- src/core/instance/lifecycle.js | 7 ++-- .../render-helpers/resolve-scoped-slots.js | 8 +++-- .../vdom/helpers/normalize-scoped-slots.js | 8 +++-- .../component/component-scoped-slot.spec.js | 30 +++++++++++++++++ 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index 1c4679f2d21..a64c3421696 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -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 @@ -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 { diff --git a/src/core/instance/lifecycle.js b/src/core/instance/lifecycle.js index 5754e3fa34a..b7265df0aaf 100644 --- a/src/core/instance/lifecycle.js +++ b/src/core/instance/lifecycle.js @@ -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 diff --git a/src/core/instance/render-helpers/resolve-scoped-slots.js b/src/core/instance/render-helpers/resolve-scoped-slots.js index 2e2abbf8186..6439324b741 100644 --- a/src/core/instance/render-helpers/resolve-scoped-slots.js +++ b/src/core/instance/render-helpers/resolve-scoped-slots.js @@ -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) { @@ -18,5 +19,8 @@ export function resolveScopedSlots ( res[slot.key] = slot.fn } } + if (contentHashKey) { + res.$key = contentHashKey + } return res } diff --git a/src/core/vdom/helpers/normalize-scoped-slots.js b/src/core/vdom/helpers/normalize-scoped-slots.js index 49c4e2effad..b2e41bf566a 100644 --- a/src/core/vdom/helpers/normalize-scoped-slots.js +++ b/src/core/vdom/helpers/normalize-scoped-slots.js @@ -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, @@ -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 } diff --git a/test/unit/features/component/component-scoped-slot.spec.js b/test/unit/features/component/component-scoped-slot.spec.js index 2aadba239b4..e12932cdd74 100644 --- a/test/unit/features/component/component-scoped-slot.spec.js +++ b/test/unit/features/component/component-scoped-slot.spec.js @@ -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: `
` + } + + const vm = new Vue({ + components: { Foo }, + data: { + ok: true + }, + template: ` +
+
+ {{ n }} +
+
+ {{ n + 1 }} +
+
+ ` + }).$mount() + + expect(vm.$el.textContent.trim()).toBe(`1`) + vm.ok = false + waitForUpdate(() => { + expect(vm.$el.textContent.trim()).toBe(`2`) + }).then(done) + }) })