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',
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 verticalAlignDebug from './debugComponents/verticalAlignDebug'
import iconTransitionDebug from './debugComponents/iconTransitionDebug'
import selectDebug from './debugComponents/selectDebug'
import hljs from 'highlight.js/lib/highlight'
import javascript from 'highlight.js/lib/languages/javascript'
@ -231,7 +232,8 @@ const routes = [
{
path: '/n-icon-transition-debug',
component: iconTransitionDebug
}
},
{ path: '/n-select-debug', component: selectDebug }
])
},
{

View File

@ -19,15 +19,7 @@
@scroll="handleMenuScroll"
>
<div class="n-base-select-menu-option-wrapper">
<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>
<n-select-menu-light-bar ref="lightBar" />
<template v-if="!loading">
<template v-if="!useSlot">
<n-select-option
@ -39,7 +31,9 @@
/>
</template>
<template v-else>
<slot />
<n-render-options :mirror="mirror">
<slot />
</n-render-options>
</template>
</template>
<div
@ -66,10 +60,11 @@
</template>
<script>
import withlightbar from '../../../mixins/withlightbar'
import NScrollbar from '../../../common/Scrollbar'
import linkedOptions from '../../../utils/data/linkedOptions'
import NSelectOption from './SelectOption'
import NSelectMenuLightBar from './SelectMenuLightBar'
import NRenderOptions from './SelectRenderOptions'
export default {
name: 'NBaseSelectMenu',
@ -80,9 +75,10 @@ export default {
},
components: {
NScrollbar,
NSelectOption
NSelectOption,
NSelectMenuLightBar,
NRenderOptions
},
mixins: [withlightbar],
props: {
theme: {
type: String,
@ -139,6 +135,10 @@ export default {
width: {
type: Number,
default: null
},
mirror: {
type: Boolean,
default: true
}
},
data () {
@ -206,6 +206,21 @@ export default {
}
},
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) {
this.$emit('menu-scroll', e, scrollContainer, scrollContent)
},
@ -217,6 +232,9 @@ export default {
this.updateLightBarPosition(e.target)
this.pendingOption = option
}
},
handleOptionMouseLeave (e, option) {
},
handleKeyUpUp () {
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>
export default {
name: 'NBaseSelectOption',
functional: true,
inject: {
NBaseSelectMenu: {
default: null
},
NBaseSelectOptionCollector: {
default: null
}
},
props: {
@ -45,60 +23,66 @@ export default {
default: false
}
},
computed: {
isSelected () {
return this.processedOption && this.NBaseSelectMenu.isSelected(this.processedOption)
},
value2Id () {
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)
render (h, context) {
const SelectMenu = context.injections.NBaseSelectMenu
let optionId = null
if (SelectMenu && SelectMenu.value2Id) {
optionId = SelectMenu.value2Id.get(context.props.value)
}
},
watch: {
value () {
this.$nextTick().then(() => {
this.NBaseSelectOptionCollector.collectOptions()
})
let id2Option = null
if (SelectMenu && SelectMenu.id2Option) {
id2Option = SelectMenu.id2Option
}
},
mounted () {
if (this.NBaseSelectOptionCollector) {
this.NBaseSelectOptionCollector.collectOptions()
let option = null
if (optionId !== null && id2Option !== null) {
option = id2Option.get(optionId)
}
},
beforeDestroy () {
this.$nextTick().then(() => {
if (this.NBaseSelectOptionCollector) {
this.NBaseSelectOptionCollector.collectOptions()
let isSelected = false
if (SelectMenu && SelectMenu.isSelected && option) {
isSelected = SelectMenu.isSelected(option)
}
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
}
})
},
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)
} else {
on = listeners
}
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>

View File

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

View File

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

View File

@ -113,7 +113,7 @@ export default {
blur: context.listeners.blur || (() => {}),
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 {
NBaseSelectMenu,
NBaseSelectOptionCollector,
NBaseSelectRenderOptions
NBaseSelectOptionCollector
} from '../../../base/SelectMenu'
export default {
@ -202,15 +201,7 @@ export default {
size: this.size,
theme: this.synthesizedTheme
}
}, [
h(NBaseSelectRenderOptions, {
scopedSlots: {
default () {
return options
}
}
})
])
}, options)
])
}
}

View File

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