diff --git a/flow/compiler.js b/flow/compiler.js
index de1e2df9..a1af7a6c 100644
--- a/flow/compiler.js
+++ b/flow/compiler.js
@@ -119,6 +119,7 @@ declare type ASTElement = {
transitionMode?: string | null;
slotName?: ?string;
slotTarget?: ?string;
+ slotTargetDynamic?: boolean;
slotScope?: ?string;
scopedSlots?: { [name: string]: ASTElement };
diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js
index bdd76be0..9f23b37e 100644
--- a/src/compiler/codegen/index.js
+++ b/src/compiler/codegen/index.js
@@ -354,11 +354,12 @@ function genScopedSlots (
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
+ const hasDynamicKeys = Object.keys(slots).some(key => slots[key].slotTargetDynamic)
return `scopedSlots:_u([${
Object.keys(slots).map(key => {
return genScopedSlot(key, slots[key], state)
}).join(',')
- }])`
+ }]${hasDynamicKeys ? `,true` : ``})`
}
function genScopedSlot (
diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js
index 1f59bb93..601dd8f2 100644
--- a/src/compiler/parser/index.js
+++ b/src/compiler/parser/index.js
@@ -586,6 +586,7 @@ function processSlotContent (el) {
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
+ el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
@@ -607,8 +608,10 @@ function processSlotContent (el) {
)
}
}
- el.slotTarget = getSlotName(slotBinding)
- el.slotScope = slotBinding.value
+ const { name, dynamic } = getSlotName(slotBinding)
+ el.slotTarget = name
+ el.slotTargetDynamic = dynamic
+ el.slotScope = slotBinding.value || `_` // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
@@ -637,10 +640,11 @@ function processSlotContent (el) {
}
// add the component's children to its default slot
const slots = el.scopedSlots || (el.scopedSlots = {})
- const target = getSlotName(slotBinding)
- const slotContainer = slots[target] = createASTElement('template', [], el)
+ const { name, dynamic } = getSlotName(slotBinding)
+ const slotContainer = slots[name] = createASTElement('template', [], el)
+ slotContainer.slotTargetDynamic = dynamic
slotContainer.children = el.children
- slotContainer.slotScope = slotBinding.value
+ slotContainer.slotScope = slotBinding.value || `_`
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
@@ -664,9 +668,9 @@ function getSlotName (binding) {
}
return dynamicKeyRE.test(name)
// dynamic [name]
- ? name.slice(1, -1)
+ ? { name: name.slice(1, -1), dynamic: true }
// static name
- : `"${name}"`
+ : { name: `"${name}"`, dynamic: false }
}
// handle outlets
diff --git a/src/core/instance/lifecycle.js b/src/core/instance/lifecycle.js
index 3879c7b5..debe94bc 100644
--- a/src/core/instance/lifecycle.js
+++ b/src/core/instance/lifecycle.js
@@ -224,12 +224,22 @@ export function updateChildComponent (
}
// determine whether component has slot children
- // we need to do this before overwriting $options._renderChildren
- const hasChildren = !!(
+ // we need to do this before overwriting $options._renderChildren.
+
+ // 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 hasDynamicScopedSlot = !!(
+ (parentVnode.data.scopedSlots && !parentVnode.data.scopedSlots.$stable) ||
+ (vm.$scopedSlots !== emptyObject && !vm.$scopedSlots.$stable)
+ )
+ // Any static slot children from the parent may have changed during parent's
+ // update. Dynamic scoped slots may also have changed. In such cases, a forced
+ // update is necessary to ensure correctness.
+ const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
- parentVnode.data.scopedSlots || // has new scoped slots
- vm.$scopedSlots !== emptyObject // has old scoped slots
+ hasDynamicScopedSlot
)
vm.$options._parentVnode = parentVnode
@@ -268,7 +278,7 @@ export function updateChildComponent (
updateComponentListeners(vm, listeners, oldListeners)
// resolve slots + force update if has children
- if (hasChildren) {
+ if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
diff --git a/src/core/instance/render-helpers/resolve-slots.js b/src/core/instance/render-helpers/resolve-slots.js
index dbec386a..53bad202 100644
--- a/src/core/instance/render-helpers/resolve-slots.js
+++ b/src/core/instance/render-helpers/resolve-slots.js
@@ -51,13 +51,14 @@ function isWhitespace (node: VNode): boolean {
export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
+ hasDynamicKeys?: boolean,
res?: Object
-): { [key: string]: Function } {
- res = res || {}
+): { [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, res)
+ resolveScopedSlots(slot, hasDynamicKeys, res)
} else {
res[slot.key] = slot.fn
}
diff --git a/src/core/vdom/helpers/normalize-scoped-slots.js b/src/core/vdom/helpers/normalize-scoped-slots.js
index 5e5f0091..aa78838e 100644
--- a/src/core/vdom/helpers/normalize-scoped-slots.js
+++ b/src/core/vdom/helpers/normalize-scoped-slots.js
@@ -14,7 +14,7 @@ export function normalizeScopedSlots (
} else {
res = {}
for (const key in slots) {
- if (slots[key]) {
+ if (slots[key] && key[0] !== '$') {
res[key] = normalizeScopedSlot(slots[key])
}
}
@@ -26,6 +26,7 @@ export function normalizeScopedSlots (
}
}
res._normalized = true
+ res.$stable = slots && slots.$stable
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 1516aadd..14d0ce4a 100644
--- a/test/unit/features/component/component-scoped-slot.spec.js
+++ b/test/unit/features/component/component-scoped-slot.spec.js
@@ -633,131 +633,69 @@ describe('Component scoped slot', () => {
})
// 2.6 new slot syntax
- if (process.env.NEW_SLOT_SYNTAX) {
- describe('v-slot syntax', () => {
- const Foo = {
- render(h) {
- return h('div', [
- this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
- this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
- this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
- ])
- }
+ describe('v-slot syntax', () => {
+ const Foo = {
+ render(h) {
+ return h('div', [
+ this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
+ this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
+ this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
+ ])
}
+ }
- const Bar = {
- render(h) {
- return this.$scopedSlots.default && this.$scopedSlots.default('from bar')
- }
+ const Bar = {
+ render(h) {
+ return this.$scopedSlots.default && this.$scopedSlots.default('from bar')
}
+ }
- const Baz = {
- render(h) {
- return this.$scopedSlots.default && this.$scopedSlots.default('from baz')
- }
+ const Baz = {
+ render(h) {
+ return this.$scopedSlots.default && this.$scopedSlots.default('from baz')
}
+ }
- const toNamed = (syntax, name) => syntax[0] === '#'
- ? `#${name}` // shorthand
- : `${syntax}:${name}` // full syntax
-
- function runSuite(syntax) {
- it('default slot', () => {
- const vm = new Vue({
- template: `{{ foo }}{{ foo }}
`,
- components: { Foo }
- }).$mount()
- expect(vm.$el.innerHTML).toBe(`from foo default
from foo default
`)
- })
-
- it('nested default slots', () => {
- const vm = new Vue({
- template: `
-
-
-
- {{ foo }} | {{ bar }} | {{ baz }}
-
-
-
- `,
- components: { Foo, Bar, Baz }
- }).$mount()
- expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
- })
-
- it('named slots', () => {
- const vm = new Vue({
- template: `
-
-
- {{ foo }}
-
-
- {{ one }}
-
-
- {{ two }}
-
-
- `,
- components: { Foo }
- }).$mount()
- expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
- })
-
- it('nested + named + default slots', () => {
- const vm = new Vue({
- template: `
-
-
-
- {{ one }} {{ bar }}
-
-
-
-
- {{ two }} {{ baz }}
-
-
-
- `,
- components: { Foo, Bar, Baz }
- }).$mount()
- expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
- })
+ const toNamed = (syntax, name) => syntax[0] === '#'
+ ? `#${name}` // shorthand
+ : `${syntax}:${name}` // full syntax
- it('should warn v-slot usage on non-component elements', () => {
- const vm = new Vue({
- template: ``
- }).$mount()
- expect(`v-slot can only be used on components or `).toHaveBeenWarned()
- })
-
- it('should warn mixed usage', () => {
- const vm = new Vue({
- template: ``,
- components: { Foo, Bar }
- }).$mount()
- expect(`Unexpected mixed usage of different slot syntaxes`).toHaveBeenWarned()
- })
- }
+ function runSuite(syntax) {
+ it('default slot', () => {
+ const vm = new Vue({
+ template: `{{ foo }}{{ foo }}
`,
+ components: { Foo }
+ }).$mount()
+ expect(vm.$el.innerHTML).toBe(`from foo defaultfrom foo default
`)
+ })
- // run tests for both full syntax and shorthand
- runSuite('v-slot')
- runSuite('#default')
+ it('nested default slots', () => {
+ const vm = new Vue({
+ template: `
+
+
+
+ {{ foo }} | {{ bar }} | {{ baz }}
+
+
+
+ `,
+ components: { Foo, Bar, Baz }
+ }).$mount()
+ expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
+ })
- it('shorthand named slots', () => {
+ it('named slots', () => {
const vm = new Vue({
template: `
-
+
{{ foo }}
-
+
{{ one }}
-
+
{{ two }}
@@ -767,62 +705,165 @@ describe('Component scoped slot', () => {
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
})
- it('should warn mixed root-default and named slots', () => {
+ it('nested + named + default slots', () => {
const vm = new Vue({
template: `
-
- {{ foo }}
-
- {{ one }}
+
+
+
+ {{ one }} {{ bar }}
+
+
+
+
+ {{ two }} {{ baz }}
+
`,
- components: { Foo }
+ components: { Foo, Bar, Baz }
}).$mount()
- expect(`default slot should also use `).toHaveBeenWarned()
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
})
- it('shorthand without scope variable', () => {
+ it('should warn v-slot usage on non-component elements', () => {
const vm = new Vue({
- template: `
-
- one
- two
-
- `,
- components: { Foo }
+ template: ``
}).$mount()
- expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`onetwo`)
+ expect(`v-slot can only be used on components or `).toHaveBeenWarned()
})
- it('shorthand named slots on root', () => {
+ it('should warn mixed usage', () => {
const vm = new Vue({
- template: `
-
- {{ one }}
-
- `,
- components: { Foo }
+ template: ``,
+ components: { Foo, Bar }
}).$mount()
- expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one`)
+ expect(`Unexpected mixed usage of different slot syntaxes`).toHaveBeenWarned()
})
+ }
- it('dynamic slot name', () => {
- const vm = new Vue({
- data: {
- a: 'one',
- b: 'two'
- },
- template: `
-
- {{ one }}
- {{ two }}
-
- `,
- components: { Foo }
- }).$mount()
- expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from foo two`)
- })
+ // run tests for both full syntax and shorthand
+ runSuite('v-slot')
+ runSuite('#default')
+
+ it('shorthand named slots', () => {
+ const vm = new Vue({
+ template: `
+
+
+ {{ foo }}
+
+
+ {{ one }}
+
+
+ {{ two }}
+
+
+ `,
+ components: { Foo }
+ }).$mount()
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
+ })
+
+ it('should warn mixed root-default and named slots', () => {
+ const vm = new Vue({
+ template: `
+
+ {{ foo }}
+
+ {{ one }}
+
+
+ `,
+ components: { Foo }
+ }).$mount()
+ expect(`default slot should also use `).toHaveBeenWarned()
+ })
+
+ it('shorthand without scope variable', () => {
+ const vm = new Vue({
+ template: `
+
+ one
+ two
+
+ `,
+ components: { Foo }
+ }).$mount()
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`onetwo`)
+ })
+
+ it('shorthand named slots on root', () => {
+ const vm = new Vue({
+ template: `
+
+ {{ one }}
+
+ `,
+ components: { Foo }
+ }).$mount()
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one`)
})
- }
+
+ it('dynamic slot name', done => {
+ const vm = new Vue({
+ data: {
+ a: 'one',
+ b: 'two'
+ },
+ template: `
+
+ a {{ one }}
+ b {{ two }}
+
+ `,
+ components: { Foo }
+ }).$mount()
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo one b from foo two`)
+ vm.a = 'two'
+ vm.b = 'one'
+ waitForUpdate(() => {
+ expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`b from foo one a from foo two `)
+ }).then(done)
+ })
+ })
+
+ // 2.6 scoped slot perf optimization
+ it('should have accurate tracking for scoped slots', done => {
+ const parentUpdate = jasmine.createSpy()
+ const childUpdate = jasmine.createSpy()
+ const vm = new Vue({
+ template: `
+ {{ parentCount }}{{ childCount }}
+ `,
+ data: {
+ parentCount: 0,
+ childCount: 0
+ },
+ updated: parentUpdate,
+ components: {
+ foo: {
+ template: `
`,
+ updated: childUpdate
+ }
+ }
+ }).$mount()
+ expect(vm.$el.innerHTML).toMatch(`00
`)
+
+ vm.parentCount++
+ waitForUpdate(() => {
+ expect(vm.$el.innerHTML).toMatch(`10
`)
+ // should only trigger parent update
+ expect(parentUpdate.calls.count()).toBe(1)
+ expect(childUpdate.calls.count()).toBe(0)
+
+ vm.childCount++
+ }).then(() => {
+ expect(vm.$el.innerHTML).toMatch(`11
`)
+ // should only trigger child update
+ expect(parentUpdate.calls.count()).toBe(1)
+ expect(childUpdate.calls.count()).toBe(1)
+ }).then(done)
+ })
})
diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js
index 7f5fbeae..d857ecbc 100644
--- a/test/unit/modules/compiler/codegen.spec.js
+++ b/test/unit/modules/compiler/codegen.spec.js
@@ -229,6 +229,13 @@ describe('codegen', () => {
)
})
+ it('generate dynamic scoped slot', () => {
+ assertCodegen(
+ '{{ bar }}',
+ `with(this){return _c('foo',{scopedSlots:_u([{key:foo,fn:function(bar){return [_v(_s(bar))]}}],true)})}`
+ )
+ })
+
it('generate scoped slot with multiline v-if', () => {
assertCodegen(
'{{ bar }}',