Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f219bed

Browse files
authoredJan 26, 2019
perf: improve scoped slots change detection accuracy (#9371)
Ensure that state mutations that only affect parent scope only trigger parent update and does not affect child components with only scoped slots.
1 parent 770c6ed commit f219bed

File tree

8 files changed

+233
-167
lines changed

8 files changed

+233
-167
lines changed
 

‎flow/compiler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ declare type ASTElement = {
119119
transitionMode?: string | null;
120120
slotName?: ?string;
121121
slotTarget?: ?string;
122+
slotTargetDynamic?: boolean;
122123
slotScope?: ?string;
123124
scopedSlots?: { [name: string]: ASTElement };
124125

‎src/compiler/codegen/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,12 @@ function genScopedSlots (
354354
slots: { [key: string]: ASTElement },
355355
state: CodegenState
356356
): string {
357+
const hasDynamicKeys = Object.keys(slots).some(key => slots[key].slotTargetDynamic)
357358
return `scopedSlots:_u([${
358359
Object.keys(slots).map(key => {
359360
return genScopedSlot(key, slots[key], state)
360361
}).join(',')
361-
}])`
362+
}]${hasDynamicKeys ? `,true` : ``})`
362363
}
363364

364365
function genScopedSlot (

‎src/compiler/parser/index.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ function processSlotContent (el) {
586586
const slotTarget = getBindingAttr(el, 'slot')
587587
if (slotTarget) {
588588
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
589+
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
589590
// preserve slot as an attribute for native shadow DOM compat
590591
// only for non-scoped slots.
591592
if (el.tag !== 'template' && !el.slotScope) {
@@ -607,8 +608,10 @@ function processSlotContent (el) {
607608
)
608609
}
609610
}
610-
el.slotTarget = getSlotName(slotBinding)
611-
el.slotScope = slotBinding.value
611+
const { name, dynamic } = getSlotName(slotBinding)
612+
el.slotTarget = name
613+
el.slotTargetDynamic = dynamic
614+
el.slotScope = slotBinding.value || `_` // force it into a scoped slot for perf
612615
}
613616
} else {
614617
// v-slot on component, denotes default slot
@@ -637,10 +640,11 @@ function processSlotContent (el) {
637640
}
638641
// add the component's children to its default slot
639642
const slots = el.scopedSlots || (el.scopedSlots = {})
640-
const target = getSlotName(slotBinding)
641-
const slotContainer = slots[target] = createASTElement('template', [], el)
643+
const { name, dynamic } = getSlotName(slotBinding)
644+
const slotContainer = slots[name] = createASTElement('template', [], el)
645+
slotContainer.slotTargetDynamic = dynamic
642646
slotContainer.children = el.children
643-
slotContainer.slotScope = slotBinding.value
647+
slotContainer.slotScope = slotBinding.value || `_`
644648
// remove children as they are returned from scopedSlots now
645649
el.children = []
646650
// mark el non-plain so data gets generated
@@ -664,9 +668,9 @@ function getSlotName (binding) {
664668
}
665669
return dynamicKeyRE.test(name)
666670
// dynamic [name]
667-
? name.slice(1, -1)
671+
? { name: name.slice(1, -1), dynamic: true }
668672
// static name
669-
: `"${name}"`
673+
: { name: `"${name}"`, dynamic: false }
670674
}
671675

672676
// handle <slot/> outlets

‎src/core/instance/lifecycle.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,22 @@ export function updateChildComponent (
224224
}
225225

226226
// determine whether component has slot children
227-
// we need to do this before overwriting $options._renderChildren
228-
const hasChildren = !!(
227+
// we need to do this before overwriting $options._renderChildren.
228+
229+
// check if there are dynamic scopedSlots (hand-written or compiled but with
230+
// dynamic slot names). Static scoped slots compiled from template has the
231+
// "$stable" marker.
232+
const hasDynamicScopedSlot = !!(
233+
(parentVnode.data.scopedSlots && !parentVnode.data.scopedSlots.$stable) ||
234+
(vm.$scopedSlots !== emptyObject && !vm.$scopedSlots.$stable)
235+
)
236+
// Any static slot children from the parent may have changed during parent's
237+
// update. Dynamic scoped slots may also have changed. In such cases, a forced
238+
// update is necessary to ensure correctness.
239+
const needsForceUpdate = !!(
229240
renderChildren || // has new static slots
230241
vm.$options._renderChildren || // has old static slots
231-
parentVnode.data.scopedSlots || // has new scoped slots
232-
vm.$scopedSlots !== emptyObject // has old scoped slots
242+
hasDynamicScopedSlot
233243
)
234244

235245
vm.$options._parentVnode = parentVnode
@@ -268,7 +278,7 @@ export function updateChildComponent (
268278
updateComponentListeners(vm, listeners, oldListeners)
269279

270280
// resolve slots + force update if has children
271-
if (hasChildren) {
281+
if (needsForceUpdate) {
272282
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
273283
vm.$forceUpdate()
274284
}

‎src/core/instance/render-helpers/resolve-slots.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ function isWhitespace (node: VNode): boolean {
5151

5252
export function resolveScopedSlots (
5353
fns: ScopedSlotsData, // see flow/vnode
54+
hasDynamicKeys?: boolean,
5455
res?: Object
55-
): { [key: string]: Function } {
56-
res = res || {}
56+
): { [key: string]: Function, $stable: boolean } {
57+
res = res || { $stable: !hasDynamicKeys }
5758
for (let i = 0; i < fns.length; i++) {
5859
const slot = fns[i]
5960
if (Array.isArray(slot)) {
60-
resolveScopedSlots(slot, res)
61+
resolveScopedSlots(slot, hasDynamicKeys, res)
6162
} else {
6263
res[slot.key] = slot.fn
6364
}

‎src/core/vdom/helpers/normalize-scoped-slots.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function normalizeScopedSlots (
1414
} else {
1515
res = {}
1616
for (const key in slots) {
17-
if (slots[key]) {
17+
if (slots[key] && key[0] !== '$') {
1818
res[key] = normalizeScopedSlot(slots[key])
1919
}
2020
}
@@ -26,6 +26,7 @@ export function normalizeScopedSlots (
2626
}
2727
}
2828
res._normalized = true
29+
res.$stable = slots && slots.$stable
2930
return res
3031
}
3132

‎test/unit/features/component/component-scoped-slot.spec.js

Lines changed: 191 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -633,131 +633,69 @@ describe('Component scoped slot', () => {
633633
})
634634

635635
// 2.6 new slot syntax
636-
if (process.env.NEW_SLOT_SYNTAX) {
637-
describe('v-slot syntax', () => {
638-
const Foo = {
639-
render(h) {
640-
return h('div', [
641-
this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
642-
this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
643-
this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
644-
])
645-
}
636+
describe('v-slot syntax', () => {
637+
const Foo = {
638+
render(h) {
639+
return h('div', [
640+
this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
641+
this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
642+
this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
643+
])
646644
}
645+
}
647646

648-
const Bar = {
649-
render(h) {
650-
return this.$scopedSlots.default && this.$scopedSlots.default('from bar')
651-
}
647+
const Bar = {
648+
render(h) {
649+
return this.$scopedSlots.default && this.$scopedSlots.default('from bar')
652650
}
651+
}
653652

654-
const Baz = {
655-
render(h) {
656-
return this.$scopedSlots.default && this.$scopedSlots.default('from baz')
657-
}
653+
const Baz = {
654+
render(h) {
655+
return this.$scopedSlots.default && this.$scopedSlots.default('from baz')
658656
}
657+
}
659658

660-
const toNamed = (syntax, name) => syntax[0] === '#'
661-
? `#${name}` // shorthand
662-
: `${syntax}:${name}` // full syntax
663-
664-
function runSuite(syntax) {
665-
it('default slot', () => {
666-
const vm = new Vue({
667-
template: `<foo ${syntax}="foo">{{ foo }}<div>{{ foo }}</div></foo>`,
668-
components: { Foo }
669-
}).$mount()
670-
expect(vm.$el.innerHTML).toBe(`from foo default<div>from foo default</div>`)
671-
})
672-
673-
it('nested default slots', () => {
674-
const vm = new Vue({
675-
template: `
676-
<foo ${syntax}="foo">
677-
<bar ${syntax}="bar">
678-
<baz ${syntax}="baz">
679-
{{ foo }} | {{ bar }} | {{ baz }}
680-
</baz>
681-
</bar>
682-
</foo>
683-
`,
684-
components: { Foo, Bar, Baz }
685-
}).$mount()
686-
expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
687-
})
688-
689-
it('named slots', () => {
690-
const vm = new Vue({
691-
template: `
692-
<foo>
693-
<template ${toNamed(syntax, 'default')}="foo">
694-
{{ foo }}
695-
</template>
696-
<template ${toNamed(syntax, 'one')}="one">
697-
{{ one }}
698-
</template>
699-
<template ${toNamed(syntax, 'two')}="two">
700-
{{ two }}
701-
</template>
702-
</foo>
703-
`,
704-
components: { Foo }
705-
}).$mount()
706-
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
707-
})
708-
709-
it('nested + named + default slots', () => {
710-
const vm = new Vue({
711-
template: `
712-
<foo>
713-
<template ${toNamed(syntax, 'one')}="one">
714-
<bar ${syntax}="bar">
715-
{{ one }} {{ bar }}
716-
</bar>
717-
</template>
718-
<template ${toNamed(syntax, 'two')}="two">
719-
<baz ${syntax}="baz">
720-
{{ two }} {{ baz }}
721-
</baz>
722-
</template>
723-
</foo>
724-
`,
725-
components: { Foo, Bar, Baz }
726-
}).$mount()
727-
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
728-
})
659+
const toNamed = (syntax, name) => syntax[0] === '#'
660+
? `#${name}` // shorthand
661+
: `${syntax}:${name}` // full syntax
729662

730-
it('should warn v-slot usage on non-component elements', () => {
731-
const vm = new Vue({
732-
template: `<div ${syntax}="foo"/>`
733-
}).$mount()
734-
expect(`v-slot can only be used on components or <template>`).toHaveBeenWarned()
735-
})
736-
737-
it('should warn mixed usage', () => {
738-
const vm = new Vue({
739-
template: `<foo><bar slot="one" slot-scope="bar" ${syntax}="bar"></bar></foo>`,
740-
components: { Foo, Bar }
741-
}).$mount()
742-
expect(`Unexpected mixed usage of different slot syntaxes`).toHaveBeenWarned()
743-
})
744-
}
663+
function runSuite(syntax) {
664+
it('default slot', () => {
665+
const vm = new Vue({
666+
template: `<foo ${syntax}="foo">{{ foo }}<div>{{ foo }}</div></foo>`,
667+
components: { Foo }
668+
}).$mount()
669+
expect(vm.$el.innerHTML).toBe(`from foo default<div>from foo default</div>`)
670+
})
745671

746-
// run tests for both full syntax and shorthand
747-
runSuite('v-slot')
748-
runSuite('#default')
672+
it('nested default slots', () => {
673+
const vm = new Vue({
674+
template: `
675+
<foo ${syntax}="foo">
676+
<bar ${syntax}="bar">
677+
<baz ${syntax}="baz">
678+
{{ foo }} | {{ bar }} | {{ baz }}
679+
</baz>
680+
</bar>
681+
</foo>
682+
`,
683+
components: { Foo, Bar, Baz }
684+
}).$mount()
685+
expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
686+
})
749687

750-
it('shorthand named slots', () => {
688+
it('named slots', () => {
751689
const vm = new Vue({
752690
template: `
753691
<foo>
754-
<template #default="foo">
692+
<template ${toNamed(syntax, 'default')}="foo">
755693
{{ foo }}
756694
</template>
757-
<template #one="one">
695+
<template ${toNamed(syntax, 'one')}="one">
758696
{{ one }}
759697
</template>
760-
<template #two="two">
698+
<template ${toNamed(syntax, 'two')}="two">
761699
{{ two }}
762700
</template>
763701
</foo>
@@ -767,62 +705,165 @@ describe('Component scoped slot', () => {
767705
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
768706
})
769707

770-
it('should warn mixed root-default and named slots', () => {
708+
it('nested + named + default slots', () => {
771709
const vm = new Vue({
772710
template: `
773-
<foo #default="foo">
774-
{{ foo }}
775-
<template #one="one">
776-
{{ one }}
711+
<foo>
712+
<template ${toNamed(syntax, 'one')}="one">
713+
<bar ${syntax}="bar">
714+
{{ one }} {{ bar }}
715+
</bar>
716+
</template>
717+
<template ${toNamed(syntax, 'two')}="two">
718+
<baz ${syntax}="baz">
719+
{{ two }} {{ baz }}
720+
</baz>
777721
</template>
778722
</foo>
779723
`,
780-
components: { Foo }
724+
components: { Foo, Bar, Baz }
781725
}).$mount()
782-
expect(`default slot should also use <template>`).toHaveBeenWarned()
726+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
783727
})
784728

785-
it('shorthand without scope variable', () => {
729+
it('should warn v-slot usage on non-component elements', () => {
786730
const vm = new Vue({
787-
template: `
788-
<foo>
789-
<template #one>one</template>
790-
<template #two>two</template>
791-
</foo>
792-
`,
793-
components: { Foo }
731+
template: `<div ${syntax}="foo"/>`
794732
}).$mount()
795-
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`onetwo`)
733+
expect(`v-slot can only be used on components or <template>`).toHaveBeenWarned()
796734
})
797735

798-
it('shorthand named slots on root', () => {
736+
it('should warn mixed usage', () => {
799737
const vm = new Vue({
800-
template: `
801-
<foo #one="one">
802-
{{ one }}
803-
</foo>
804-
`,
805-
components: { Foo }
738+
template: `<foo><bar slot="one" slot-scope="bar" ${syntax}="bar"></bar></foo>`,
739+
components: { Foo, Bar }
806740
}).$mount()
807-
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one`)
741+
expect(`Unexpected mixed usage of different slot syntaxes`).toHaveBeenWarned()
808742
})
743+
}
809744

810-
it('dynamic slot name', () => {
811-
const vm = new Vue({
812-
data: {
813-
a: 'one',
814-
b: 'two'
815-
},
816-
template: `
817-
<foo>
818-
<template #[a]="one">{{ one }} </template>
819-
<template v-slot:[b]="two">{{ two }}</template>
820-
</foo>
821-
`,
822-
components: { Foo }
823-
}).$mount()
824-
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from foo two`)
825-
})
745+
// run tests for both full syntax and shorthand
746+
runSuite('v-slot')
747+
runSuite('#default')
748+
749+
it('shorthand named slots', () => {
750+
const vm = new Vue({
751+
template: `
752+
<foo>
753+
<template #default="foo">
754+
{{ foo }}
755+
</template>
756+
<template #one="one">
757+
{{ one }}
758+
</template>
759+
<template #two="two">
760+
{{ two }}
761+
</template>
762+
</foo>
763+
`,
764+
components: { Foo }
765+
}).$mount()
766+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
767+
})
768+
769+
it('should warn mixed root-default and named slots', () => {
770+
const vm = new Vue({
771+
template: `
772+
<foo #default="foo">
773+
{{ foo }}
774+
<template #one="one">
775+
{{ one }}
776+
</template>
777+
</foo>
778+
`,
779+
components: { Foo }
780+
}).$mount()
781+
expect(`default slot should also use <template>`).toHaveBeenWarned()
782+
})
783+
784+
it('shorthand without scope variable', () => {
785+
const vm = new Vue({
786+
template: `
787+
<foo>
788+
<template #one>one</template>
789+
<template #two>two</template>
790+
</foo>
791+
`,
792+
components: { Foo }
793+
}).$mount()
794+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`onetwo`)
795+
})
796+
797+
it('shorthand named slots on root', () => {
798+
const vm = new Vue({
799+
template: `
800+
<foo #one="one">
801+
{{ one }}
802+
</foo>
803+
`,
804+
components: { Foo }
805+
}).$mount()
806+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one`)
826807
})
827-
}
808+
809+
it('dynamic slot name', done => {
810+
const vm = new Vue({
811+
data: {
812+
a: 'one',
813+
b: 'two'
814+
},
815+
template: `
816+
<foo>
817+
<template #[a]="one">a {{ one }} </template>
818+
<template v-slot:[b]="two">b {{ two }} </template>
819+
</foo>
820+
`,
821+
components: { Foo }
822+
}).$mount()
823+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo one b from foo two`)
824+
vm.a = 'two'
825+
vm.b = 'one'
826+
waitForUpdate(() => {
827+
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`b from foo one a from foo two `)
828+
}).then(done)
829+
})
830+
})
831+
832+
// 2.6 scoped slot perf optimization
833+
it('should have accurate tracking for scoped slots', done => {
834+
const parentUpdate = jasmine.createSpy()
835+
const childUpdate = jasmine.createSpy()
836+
const vm = new Vue({
837+
template: `
838+
<div>{{ parentCount }}<foo #default>{{ childCount }}</foo></div>
839+
`,
840+
data: {
841+
parentCount: 0,
842+
childCount: 0
843+
},
844+
updated: parentUpdate,
845+
components: {
846+
foo: {
847+
template: `<div><slot/></div>`,
848+
updated: childUpdate
849+
}
850+
}
851+
}).$mount()
852+
expect(vm.$el.innerHTML).toMatch(`0<div>0</div>`)
853+
854+
vm.parentCount++
855+
waitForUpdate(() => {
856+
expect(vm.$el.innerHTML).toMatch(`1<div>0</div>`)
857+
// should only trigger parent update
858+
expect(parentUpdate.calls.count()).toBe(1)
859+
expect(childUpdate.calls.count()).toBe(0)
860+
861+
vm.childCount++
862+
}).then(() => {
863+
expect(vm.$el.innerHTML).toMatch(`1<div>1</div>`)
864+
// should only trigger child update
865+
expect(parentUpdate.calls.count()).toBe(1)
866+
expect(childUpdate.calls.count()).toBe(1)
867+
}).then(done)
868+
})
828869
})

‎test/unit/modules/compiler/codegen.spec.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ describe('codegen', () => {
229229
)
230230
})
231231

232+
it('generate dynamic scoped slot', () => {
233+
assertCodegen(
234+
'<foo><template :slot="foo" slot-scope="bar">{{ bar }}</template></foo>',
235+
`with(this){return _c('foo',{scopedSlots:_u([{key:foo,fn:function(bar){return [_v(_s(bar))]}}],true)})}`
236+
)
237+
})
238+
232239
it('generate scoped slot with multiline v-if', () => {
233240
assertCodegen(
234241
'<foo><template v-if="\nshow\n" slot-scope="bar">{{ bar }}</template></foo>',

0 commit comments

Comments
 (0)
Please sign in to comment.