فهرست منبع

fix: ensure scoped slots update in conditional branches

close #9534
Evan You 7 سال پیش
والد
کامیت
d9b27a92bd

+ 28 - 5
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 {

+ 5 - 2
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

+ 6 - 2
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
 }

+ 6 - 2
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
 }
 

+ 30 - 0
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: `<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)
+  })
 })