element select實(shí)現(xiàn)組件虛擬滾動(dòng)優(yōu)化
不知道大家在開發(fā)過程中有沒有遇到這樣一個(gè)場景,后端接口一次性返回上千條數(shù)據(jù)(比如國家地區(qū)),接口不支持分頁,不能篩選,只能前端自己通過 select 組件全量渲染出來。這種渲染大量 DOM 的場景下會(huì)造成頁面非??D,我在網(wǎng)上搜索了一下一般有兩種解決方案
- 前端自己實(shí)現(xiàn)數(shù)據(jù)分頁效果
- 虛擬滾動(dòng),比如 element plus 就有專門的 select 虛擬滾動(dòng)組件
最好的方案當(dāng)然時(shí)直接使用成熟的輪子,奈何我們的項(xiàng)目是 vue 2.7,所以只能借助支持 vue2 的虛擬滾動(dòng)組件 vue-virtual-scroll-list,自己封裝一個(gè) select 虛擬滾動(dòng)組件
組件實(shí)現(xiàn)
首先在項(xiàng)目中引入 vue-virtual-scroll-list
npm i vue-virtual-scroll-list
接下來開發(fā)封裝虛擬滾動(dòng)組件,因?yàn)槭褂玫氖?vue2.7 版本,為了以后項(xiàng)目升級(jí) vue3,所以直接使用 composition api 的方式開發(fā)。在 el-select 組件內(nèi)部引入安裝好的 vue-virtual-scroll-list,定義好組件的基礎(chǔ)結(jié)構(gòu),和需要傳入的 props 屬性
<template>
<el-select
v-model="value"
v-bind="$atts"
v-on="$listeners"
>
<virtual-scroll-list
ref="virtualListRef"
></virtual-scroll-list>
</el-select>
</template>
<script setup>
import VirtualScrollList from 'vue-virtual-scroll-list'
const props = defineProps({
// 當(dāng)前
value: {
type: [String, Number],
default: '',
},
// 下拉展示的 options
options: {
type: Array,
default: () => [],
},
// label 鍵值
labelKey: {
type: String,
default: 'label',
},
// value 鍵值
valueKey: {
type: String,
default: 'value',
},
})
const { value, options, labelKey, valueKey } = toRefs(props)
const virtualListRef = ref(null)
</script>
根據(jù)官網(wǎng)文檔描述,有三個(gè)必填屬性,data-key, data-sources, data-component,我們可以直接選取 value 作為唯一的 data-key,data-sources 就是我們傳入的 options,data-component 需要我們將 el-option 封裝為一個(gè)獨(dú)立的組件
| 屬性 | 是否必填 | 默認(rèn)值 | 類型 | 描述 |
|---|---|---|---|---|
data-key | 必填 | String | Function | 虛擬滾動(dòng)列表每一項(xiàng)的唯一 id,如果是函數(shù)的話需要返回 string | |
data-sources | 必填 | Array[Object] | 虛擬滾動(dòng)的數(shù)據(jù)列表,每個(gè)數(shù)組項(xiàng)必須是對(duì)象,并且每個(gè)對(duì)象必須有一個(gè)唯一的屬性與 data-key 匹配 | |
data-component | 必填 | Component | 虛擬滾動(dòng)每一項(xiàng)的渲染組件 | |
keeps | 非必填 | 30 | Number | 虛擬列表展示的真實(shí) DOM 的數(shù)量 |
extra-props | 非必填 | {} | Object | 傳遞給子組件的額外參數(shù) |
我們先將 el-option 封裝為一個(gè)獨(dú)立組件,需要注意的是 vue-virtual-scroll-list 默認(rèn)傳入是數(shù)組是 source,所以需要從 source 屬性中根據(jù) labelKey 和 valueKey 找到需要加載的 label 和 value
<template>
<el-option
:key="value"
:label="label"
:value="value"
v-bind="$atts"
v-on="$listeners"
/>
</template>
<script setup>
import { computed, toRefs } from 'vue'
const props = defineProps({
source: {
type: Object,
default: () => {},
},
valueKey: {
type: [String, Number],
default: '',
},
labelKey: {
type: String,
default: '',
},
})
const { source, valueKey, labelKey } = toRefs(props)
const value = computed(() => source.value[valueKey.value])
const label = computed(() => source.value[labelKey.value])
</script>
接著在父組件中引入封裝的 el-option 組件,這里我們?nèi)∶麨?OptionNode,然后傳入 data-key, data-sources, data-component 三個(gè)必填屬性,同時(shí)將 labelKey 和 valueKey 通過 extra-props 屬性傳遞給子組件。vue-virtual-scroll-list 組件需要顯式設(shè)置列表的高度和滾動(dòng)條,不然在元素過多時(shí)會(huì)出現(xiàn)列表過長的情況,同時(shí)為了配合項(xiàng)目,這里我將 keeps 屬性設(shè)置為 20,也就是只渲染 20 個(gè)真實(shí) DOM 節(jié)點(diǎn)
<template>
<fe-select
v-model="value"
v-bind="$atts"
v-on="$listeners"
>
<virtual-scroll-list
ref="virtualListRef"
+ class="virtual-scroll-list"
+ :data-key="dataKey"
+ :data-sources="allOptions"
+ :data-component="OptionNode"
+ :keeps="20"
+ :extra-props="{
+ labelKey,
+ valueKey,
+ }"
></virtual-scroll-list>
</fe-select>
</template>
<script setup>
import { toRefs, ref, nextTick, computed, watch, onMounted } from 'vue'
import VirtualScrollList from 'vue-virtual-scroll-list'
+ import OptionNode from './option-node.vue'
const props = defineProps({
value: {
type: [String, Number],
default: '',
},
options: {
type: Array,
default: () => [],
},
labelKey: {
type: String,
default: 'label',
},
valueKey: {
type: String,
default: 'value',
},
})
const { value, options, labelKey, valueKey } = toRefs(props)
const virtualListRef = ref(null)
+ const dataKey = ref(valueKey)
+ const allOptions = options.value
</script>
+ <style lang="scss" scoped>
+ .virtual-scroll-list {
+ height: 200px;
+ overflow: auto;
+ }
+ </style>
這個(gè)時(shí)候就可以初步實(shí)現(xiàn)虛擬列表滾動(dòng)的效果了,但由于虛擬滾動(dòng)僅渲染部分 DOM,所有還有兩個(gè)問題需要考慮
- 保存了選擇項(xiàng)后,再二次加載時(shí),需要顯示保存項(xiàng),這時(shí)需要從完整 options 中找到保存項(xiàng)并放在列表最上面
- 篩選列表選項(xiàng)時(shí),需要從完整 options 找到符合要求的選項(xiàng)并加載,同時(shí)在關(guān)閉列表時(shí),需要重置列表
針對(duì)以上兩個(gè)問題,我們引入一個(gè) currentOptions 記錄當(dāng)前的 options,通過 remote-method 實(shí)現(xiàn)搜索效果,通過 visible-change 事件實(shí)現(xiàn)重置列表。經(jīng)過優(yōu)化后的代碼如下
<template>
<fe-select
v-model="value"
+ filterable
+ remote
+ :remote-method="handleRemoteMethod"
v-bind="$atts"
+ @visible-change="handleVisiableChange"
v-on="$listeners"
>
<virtual-scroll-list
ref="virtualListRef"
class="virtual-scroll-list"
:data-key="dataKey"
:data-sources="currentOptions"
:data-component="OptionNode"
:keeps="20"
:extra-props="{
labelKey,
valueKey,
}"
></virtual-scroll-list>
</fe-select>
</template>
<script setup>
import { toRefs, ref, nextTick, computed, watch, onMounted } from 'vue'
import VirtualScrollList from 'vue-virtual-scroll-list'
import OptionNode from './option-node.vue'
+ import { cloneDeep, isNil } from 'lodash-es'
const props = defineProps({
value: {
type: [String, Number],
default: '',
},
options: {
type: Array,
default: () => [],
},
labelKey: {
type: String,
default: 'label',
},
valueKey: {
type: String,
default: 'value',
},
})
const { value, options, labelKey, valueKey } = toRefs(props)
const virtualListRef = ref(null)
const dataKey = ref(valueKey)
+ // 當(dāng)前篩選的 options
+ const currentOptions = ref([])
+ // 全量 options
+ // 注意這里需要深拷貝
+ const allOptions = computed(() => cloneDeep(options.value))
+ onMounted(() => {
+ handleInitOptions(allOptions.value, value.value)
+ })
+ watch([value, options], ([newVal, newOptions], [_oldVal, oldOptions]) => {
+ // 異步加載 options 時(shí),如果 value 有值,需要將 value 對(duì)應(yīng)的 option 放在第一位
+ if ((!isNil(newVal) || newVal !== '') && newOptions.length > 0 && oldOptions.length === 0) {
+ handleInitOptions(newOptions, newVal)
+ }
+ })
+ /**
+ * @description 因?yàn)?DOM 不是全量加載,所以需要手動(dòng)處理
+ */
+ function handleRemoteMethod(query) {
+ if (query !== '') {
+ currentOptions.value = allOptions.value.filter((item) => {
+ return item[labelKey.value].includes(query)
+ })
+ } else {
+ currentOptions.value = allOptions.value
+ }
+ }
+ function handleVisiableChange(val) {
+ // 隱藏下拉框時(shí),重置數(shù)據(jù)
+ if (!val) {
+ virtualListRef.value && virtualListRef.value.reset()
+ nextTick(() => {
+ currentOptions.value = allOptions.value
+ })
+ }
+ }
+ /**
+ * @description 異步加載 options 時(shí),如果 value 有值,需要將 value 對(duì)應(yīng)的 option 放在第一位
+ */
+ function handleInitOptions(allOptions, value) {
+ const existOption = allOptions.find((item) => {
+ return item[valueKey.value] === value
+ })
+ if (existOption) {
+ currentOptions.value.push(existOption)
+ }
+ currentOptions.value.push(
+ ...allOptions.filter((item) => {
+ return item[valueKey.value] !== value
+ })
+ )
+ }
</script>
<style lang="scss" scoped>
.virtual-scroll-list {
height: 200px;
overflow: auto;
}
</style>
總結(jié)
最后我們來看一下最終實(shí)現(xiàn)效果,可以看到在實(shí)際選項(xiàng)有 10000 個(gè)情況下,每次渲染出來的 DOM 只有 20 個(gè),而且不論是滾動(dòng)還是查詢都絲滑流暢,完成的組件封裝代碼和示例我也放在 github 上(鏈接)

到此這篇關(guān)于element select實(shí)現(xiàn)組件虛擬滾動(dòng)優(yōu)化的文章就介紹到這了,更多相關(guān)element select組件虛擬滾動(dòng)優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于vuejs中v-if和v-show的區(qū)別及v-show不起作用問題
v-if 有更高的切換開銷,而 v-show 有更高的出事渲染開銷.因此,如果需要非常頻繁的切換,那么使用v-show好一點(diǎn);如果在運(yùn)行時(shí)條件不太可能改變,則使用v-if 好點(diǎn)2018-03-03
Vue中實(shí)現(xiàn)滾動(dòng)加載與無限滾動(dòng)
本文主要介紹了Vue中實(shí)現(xiàn)滾動(dòng)加載與無限滾動(dòng),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
詳解vue-router 動(dòng)態(tài)路由下子頁面多頁共活的解決方案
這篇文章主要介紹了vue-router 動(dòng)態(tài)路由下子頁面多頁共活的解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
記一次vue-webpack項(xiàng)目優(yōu)化實(shí)踐詳解
這篇文章主要介紹了記一次vue-webpack項(xiàng)目優(yōu)化實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-02-02
vue3.2+ts實(shí)現(xiàn)在方法中可調(diào)用的擬態(tài)框彈窗(類el-MessageBox)
這篇文章主要介紹了vue3.2+ts實(shí)現(xiàn)在方法中可調(diào)用的擬態(tài)框彈窗(類el-MessageBox),這個(gè)需求最主要的是要通過方法去調(diào)用,為了像el-messagebox使用那樣方便,需要的朋友可以參考下2022-12-12

