refactor(select): use functional select option to significantly imporve render preformance

This commit is contained in:
07akioni 2019-12-25 19:51:31 +08:00
parent b7c13912f5
commit cbfc200dc1
11 changed files with 200 additions and 136 deletions

View File

@ -0,0 +1,41 @@
<!--template>
<n-select v-model="value" :options="options" />
</template-->
<template>
<!-- <n-select v-model="value">
<n-select-option label="label1" value="value1">
Dog
</n-select-option>
<n-select-option label="label2" value="value2">
Cat
</n-select-option>
<n-select-option label="label3" value="value3">
Money
</n-select-option>
<n-select-option label="label4" value="value4">
Tiger
</n-select-option>
<n-select-option label="label5" value="value5">
Happy
</n-select-option>
</n-select> -->
<n-select v-model="value" :options="options" />
</template>
<script>
export default {
data () {
const options = []
for (let i = 0; i < 2500; ++i) {
options.push({
label: String(i),
value: i
})
}
return {
options,
value: null
}
}
}
</script>

View File

@ -392,6 +392,10 @@ export default {
{ {
name: 'IconTransitionDebug', name: 'IconTransitionDebug',
path: `/${this.lang}/${this.theme}` + '/n-icon-transition-debug' path: `/${this.lang}/${this.theme}` + '/n-icon-transition-debug'
},
{
name: 'SelectDebug',
path: `/${this.lang}/${this.theme}` + '/n-select-debug'
} }
] ]
} }

View File

@ -92,6 +92,7 @@ import cancelMarkDebug from './debugComponents/cancelMarkDebug'
import cascaderDebug from './debugComponents/cascaderDebug' import cascaderDebug from './debugComponents/cascaderDebug'
import verticalAlignDebug from './debugComponents/verticalAlignDebug' import verticalAlignDebug from './debugComponents/verticalAlignDebug'
import iconTransitionDebug from './debugComponents/iconTransitionDebug' import iconTransitionDebug from './debugComponents/iconTransitionDebug'
import selectDebug from './debugComponents/selectDebug'
import hljs from 'highlight.js/lib/highlight' import hljs from 'highlight.js/lib/highlight'
import javascript from 'highlight.js/lib/languages/javascript' import javascript from 'highlight.js/lib/languages/javascript'
@ -231,7 +232,8 @@ const routes = [
{ {
path: '/n-icon-transition-debug', path: '/n-icon-transition-debug',
component: iconTransitionDebug component: iconTransitionDebug
} },
{ path: '/n-select-debug', component: selectDebug }
]) ])
}, },
{ {

View File

@ -19,15 +19,7 @@
@scroll="handleMenuScroll" @scroll="handleMenuScroll"
> >
<div class="n-base-select-menu-option-wrapper"> <div class="n-base-select-menu-option-wrapper">
<div class="n-base-select-menu-light-bar-wrapper"> <n-select-menu-light-bar ref="lightBar" />
<transition name="n-base-select-menu-light-bar--transition">
<div
v-if="showLightBar"
class="n-base-select-menu-light-bar"
:style="{ top: `${lightBarTop}px` }"
/>
</transition>
</div>
<template v-if="!loading"> <template v-if="!loading">
<template v-if="!useSlot"> <template v-if="!useSlot">
<n-select-option <n-select-option
@ -39,7 +31,9 @@
/> />
</template> </template>
<template v-else> <template v-else>
<slot /> <n-render-options :mirror="mirror">
<slot />
</n-render-options>
</template> </template>
</template> </template>
<div <div
@ -66,10 +60,11 @@
</template> </template>
<script> <script>
import withlightbar from '../../../mixins/withlightbar'
import NScrollbar from '../../../common/Scrollbar' import NScrollbar from '../../../common/Scrollbar'
import linkedOptions from '../../../utils/data/linkedOptions' import linkedOptions from '../../../utils/data/linkedOptions'
import NSelectOption from './SelectOption' import NSelectOption from './SelectOption'
import NSelectMenuLightBar from './SelectMenuLightBar'
import NRenderOptions from './SelectRenderOptions'
export default { export default {
name: 'NBaseSelectMenu', name: 'NBaseSelectMenu',
@ -80,9 +75,10 @@ export default {
}, },
components: { components: {
NScrollbar, NScrollbar,
NSelectOption NSelectOption,
NSelectMenuLightBar,
NRenderOptions
}, },
mixins: [withlightbar],
props: { props: {
theme: { theme: {
type: String, type: String,
@ -139,6 +135,10 @@ export default {
width: { width: {
type: Number, type: Number,
default: null default: null
},
mirror: {
type: Boolean,
default: true
} }
}, },
data () { data () {
@ -206,6 +206,21 @@ export default {
} }
}, },
methods: { methods: {
hideLightBar () {
if (this.$refs.lightBar) {
this.$refs.lightBar.hideLightBar()
}
},
hideLightBarSync () {
if (this.$refs.lightBar) {
this.$refs.lightBar.hideLightBarSync()
}
},
updateLightBarPosition (el) {
if (this.$refs.lightBar) {
this.$refs.lightBar.updateLightBarPosition(el)
}
},
handleMenuScroll (e, scrollContainer, scrollContent) { handleMenuScroll (e, scrollContainer, scrollContent) {
this.$emit('menu-scroll', e, scrollContainer, scrollContent) this.$emit('menu-scroll', e, scrollContainer, scrollContent)
}, },
@ -217,6 +232,9 @@ export default {
this.updateLightBarPosition(e.target) this.updateLightBarPosition(e.target)
this.pendingOption = option this.pendingOption = option
} }
},
handleOptionMouseLeave (e, option) {
}, },
handleKeyUpUp () { handleKeyUpUp () {
this.prev() this.prev()

View File

@ -0,0 +1,25 @@
<template>
<div class="n-base-select-menu-light-bar-wrapper">
<transition name="n-base-select-menu-light-bar--transition">
<div
v-if="showLightBar"
class="n-base-select-menu-light-bar"
:style="{ top: `${lightBarTop}px` }"
/>
</transition>
</div>
</template>
<script>
import withlightbar from '../../../mixins/withlightbar'
export default {
name: 'NBaseSelectMenuLightBar',
mixins: [ withlightbar ],
data () {
return {
active: true
}
}
}
</script>

View File

@ -1,32 +1,10 @@
<template>
<div
class="n-base-select-option"
:class="{
'n-base-select-option--selected':
isSelected,
'n-base-select-option--disabled':
disabled
}"
:data-id="optionId"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<slot>
{{ label }}
</slot>
</div>
</template>
<script> <script>
export default { export default {
name: 'NBaseSelectOption', name: 'NBaseSelectOption',
functional: true,
inject: { inject: {
NBaseSelectMenu: { NBaseSelectMenu: {
default: null default: null
},
NBaseSelectOptionCollector: {
default: null
} }
}, },
props: { props: {
@ -45,60 +23,66 @@ export default {
default: false default: false
} }
}, },
computed: { render (h, context) {
isSelected () { const SelectMenu = context.injections.NBaseSelectMenu
return this.processedOption && this.NBaseSelectMenu.isSelected(this.processedOption) let optionId = null
}, if (SelectMenu && SelectMenu.value2Id) {
value2Id () { optionId = SelectMenu.value2Id.get(context.props.value)
if (this.NBaseSelectMenu) {
return this.NBaseSelectMenu.value2Id
} return null
},
id2Option () {
if (this.NBaseSelectMenu) {
return this.NBaseSelectMenu.id2Option
} return null
},
optionId () {
if (this.value2Id === null) return null
return this.value2Id.get(this.value)
},
processedOption () {
if (this.id2Option === null) return null
return this.id2Option.get(this.optionId)
} }
}, let id2Option = null
watch: { if (SelectMenu && SelectMenu.id2Option) {
value () { id2Option = SelectMenu.id2Option
this.$nextTick().then(() => {
this.NBaseSelectOptionCollector.collectOptions()
})
} }
}, let option = null
mounted () { if (optionId !== null && id2Option !== null) {
if (this.NBaseSelectOptionCollector) { option = id2Option.get(optionId)
this.NBaseSelectOptionCollector.collectOptions()
} }
}, let isSelected = false
beforeDestroy () { if (SelectMenu && SelectMenu.isSelected && option) {
this.$nextTick().then(() => { isSelected = SelectMenu.isSelected(option)
if (this.NBaseSelectOptionCollector) { }
this.NBaseSelectOptionCollector.collectOptions() const listeners = context.listeners || {}
function handleClick (e) {
SelectMenu.handleOptionClick(e, option)
listeners.click && listeners.click(e)
}
function handleMouseEnter (e) {
SelectMenu.handleOptionMouseEnter(e, option)
listeners.mouseenter && listeners.mouseenter(e)
}
function handleMouseLeave (e) {
SelectMenu.handleOptionMouseLeave(e, option)
listeners.mouseleave && listeners.mouseleave(e)
}
let on = {}
if (SelectMenu) {
on = {
click: handleClick,
mouseenter: handleMouseEnter,
mouseleave: handleMouseLeave
} }
}) } else {
}, on = listeners
methods: {
handleClick (e) {
this.NBaseSelectMenu.handleOptionClick(e, this.processedOption)
this.$emit('click', e)
},
handleMouseEnter (e) {
this.NBaseSelectMenu.handleOptionMouseEnter(e, this.processedOption)
this.$emit('mouseenter', e)
},
handleMouseLeave (e) {
this.$emit('mouseleave', e)
} }
let attrs = {}
if (optionId !== null) {
attrs = {
'data-id': optionId
}
}
return h('div', {
staticClass: 'n-base-select-option',
class: {
'n-base-select-option--selected': isSelected,
'n-base-select-option--disabled': context.props.disabled
},
key: context.props.value,
attrs,
on,
props: {
...context.props
}
}, (context.scopedSlots.default && context.scopedSlots.default()) || [ context.props.label ])
} }
} }
</script> </script>

View File

@ -63,11 +63,13 @@ export default {
const children = getDefaultSlotOf(this) const children = getDefaultSlotOf(this)
children.forEach((child, index) => { children.forEach((child, index) => {
child.key = index child.key = index
if (child.componentOptions) { /**
if (VALID_COMPONENT.includes(getComponentNameOf(child))) { * If component name is valid,
const propsData = getOptionPropsDataOf(child) * there must be data
this.options.push(propsData) */
} if (VALID_COMPONENT.includes(getComponentNameOf(child))) {
const propsData = getOptionPropsDataOf(child)
this.options.push({ ...propsData, children: child.children })
} }
}) })
} }

View File

@ -1,37 +1,38 @@
<script> <script>
import { import SelectOption from './SelectOption'
getComponentNameOf,
getOptionPropsDataOf
} from '../../../utils/component'
import {
VALID_COMPONENT
} from './config'
export default { export default {
name: 'NSelectRenderOptions', name: 'NSelectRenderOptions',
functional: true, functional: true,
inject: {
NBaseSelectMenu: {
default: null
},
mirror: {
type: Boolean,
default: true
}
},
render (h, context) { render (h, context) {
const defaultSlot = context.scopedSlots.default() if (context.props.mirror) {
const filteredDefaultSlot = defaultSlot return context.children
.filter(vNode => { } else {
if (VALID_COMPONENT.includes(getComponentNameOf(vNode))) { const selectMenu = context.injections.NBaseSelectMenu
if (vNode.componentOptions) { const options = (selectMenu && selectMenu.linkedOptions) || []
const filter = context.props.filter return options.map(option => {
const filterable = context.props.filterable return h(SelectOption, {
const remote = context.props.remote props: {
if (!remote && filterable && filter) { label: option.label,
const pattern = context.props.pattern value: option.value
const option = getOptionPropsDataOf(vNode) },
return filter(pattern, option) scopedSlots: {
default () {
return option.children
} }
return true
} }
return true })
}
return true
}) })
return filteredDefaultSlot }
} }
} }
</script> </script>

View File

@ -113,7 +113,7 @@ export default {
blur: context.listeners.blur || (() => {}), blur: context.listeners.blur || (() => {}),
select: context.listeners.select || (() => {}) select: context.listeners.select || (() => {})
}, },
scopedSlots: context.scopedSlots scopedSlots: { ...context.scopedSlots }
}) })
} }
}, },

View File

@ -5,8 +5,7 @@ import asthemecontext from '../../../mixins/asthemecontext'
import WrapWithValue from './WrapWithValue' import WrapWithValue from './WrapWithValue'
import { import {
NBaseSelectMenu, NBaseSelectMenu,
NBaseSelectOptionCollector, NBaseSelectOptionCollector
NBaseSelectRenderOptions
} from '../../../base/SelectMenu' } from '../../../base/SelectMenu'
export default { export default {
@ -202,15 +201,7 @@ export default {
size: this.size, size: this.size,
theme: this.synthesizedTheme theme: this.synthesizedTheme
} }
}, [ }, options)
h(NBaseSelectRenderOptions, {
scopedSlots: {
default () {
return options
}
}
})
])
]) ])
} }
} }

View File

@ -64,7 +64,6 @@
:options="filteredOptions" :options="filteredOptions"
:multiple="multiple" :multiple="multiple"
:size="size" :size="size"
:remote="remote"
:loading="loading" :loading="loading"
:no-data-content="noDataContent" :no-data-content="noDataContent"
:not-found-content="notFoundContent" :not-found-content="notFoundContent"
@ -72,12 +71,11 @@
:filterable="filterable" :filterable="filterable"
:is-selected="isSelected" :is-selected="isSelected"
:use-slot="useSlot" :use-slot="useSlot"
:mirror="false"
@menu-toggle-option="handleToggleOption" @menu-toggle-option="handleToggleOption"
@menu-scroll="handleMenuScroll" @menu-scroll="handleMenuScroll"
> >
<n-base-select-render-options v-if="useSlot" :filterable="filterable" :remote="remote" :filter="filter" :pattern="pattern"> <slot />
<slot />
</n-base-select-render-options>
</n-base-select-menu> </n-base-select-menu>
</transition> </transition>
</div> </div>
@ -93,8 +91,7 @@ import zindexable from '../../../mixins/zindexable'
import clickoutside from '../../../directives/clickoutside' import clickoutside from '../../../directives/clickoutside'
import { import {
NBaseSelectMenu, NBaseSelectMenu,
NBaseSelectOptionCollector, NBaseSelectOptionCollector
NBaseSelectRenderOptions
} from '../../../base/SelectMenu' } from '../../../base/SelectMenu'
import NBasePicker from '../../../base/Picker' import NBasePicker from '../../../base/Picker'
import withapp from '../../../mixins/withapp' import withapp from '../../../mixins/withapp'
@ -114,8 +111,7 @@ export default {
components: { components: {
NBaseSelectMenu, NBaseSelectMenu,
NBasePicker, NBasePicker,
NBaseSelectOptionCollector, NBaseSelectOptionCollector
NBaseSelectRenderOptions
}, },
directives: { directives: {
clickoutside clickoutside