Просмотр исходного кода

fix: bail out scoped slot optimization when there are nested scopes

fix #9438
Evan You 7 лет назад
Родитель
Сommit
4d4d22a3f6

+ 26 - 4
src/compiler/codegen/index.js

@@ -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;
@@ -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) {
@@ -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 (
@@ -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`

+ 10 - 3
src/compiler/parser/index.js

@@ -44,6 +44,8 @@ const invalidAttributeRE = /[\s"'<>\/=]/
 
 const decodeHTMLCached = cached(he.decode)
 
+export const emptySlotScopeToken = `_empty_`
+
 // configurable state
 export let warn: any
 let delimiters
@@ -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
@@ -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

+ 35 - 0
test/unit/features/component/component-scoped-slot.spec.js

@@ -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)
+  })
 })