优化实战 第 12 期 - 下拉框数据量过大的卡顿优化

2,743 阅读1分钟

性能分析

当服务端返回的选择项数据达到一定量级的时候,加载渲染时会异常的卡顿,甚至会引发整个页面的卡顿

此时的用户体验是及差的

优化方案

对数据进行处理,只展示前 n 百条数据,通过自定义检索可以检索所有的数据

由于初始化拉取的数据量相对来说比较大,所以将数据的拉取放到工作线程去处理,详情可参照 第05期 - 多线程共享Worker实现数据预获取

组件封装

  • 配置项设计

    export default {
      props: {
        data: {
          type: Array, required: true  // 源数据
        },
        count: {
          type: Number, default: 100  // 前 n 百条数据
        },
        idAlias: {
          type: String, default: 'id'  // 设置业务字段名称
        },
        nameAlias: {
          type: String, default: 'name'  // 设置显示字段名称
        },
        placeholder: {
          type: String, default: '请选择'  // 设置占位文本
        }
      }
    }
    
  • 使用组件的 v-model 实现父子组件数据同步

    父组件

    <base-select :data="data" v-model="performer" />
    

    子组件

    <template>
      <el-select v-model="value" @change="handleChange">
        <el-option 
          v-for="item of options" :key="item[idAlias]" 
          :label="item[nameAlias]" 
          :value="item[idAlias]">
        </el-option>
      </el-select>
    </template>
    
    export default {
      model: {
        prop: 'value',
        event: 'change'
      },
      props: {
        value: {
          type: Array,
          required: true
        },
      },
      methods: {
        handleChange(value) {
          this.$emit('change', value)
        },
      }
    }
    
  • 通过监听对源数据进行初始化(考虑数据回显的问题)

    export default {
      data() {
        return {
          options: []
        }
      },
      watch: {
        value: {
          handler(newVal, oldVal) {
            newVal && this.init()
          },
          immediate: true
        }
      },
      methods: {
        init() {
          const { data, count, idAlias, value: selectedIds } = this
          const allreadySelectedItems = data.filter(item => selectedIds.includes(item[idAlias]))
          const records = data.filter(item => !selectedIds.includes(item[idAlias])).slice(0, count - allreadySelectedItems.length)
          this.options = [...allreadySelectedItems, ...records]
        }
      }
    }
    

    把已选择的数据项放到最前边,方便后续选项多的优化扩展,只需要给模板添加 collapse-tags 属性即可

  • 实现自定义检索

    <template>
      <el-select v-model="value" :filter-method="filterMethod">
        <el-option 
          v-for="item of options" :key="item[idAlias]" 
          :label="item[nameAlias]" 
          :value="item[idAlias]">
        </el-option>
      </el-select>
    </template>
    
    export default {
      data() {
        return {
          options: []
        }
      },
      methods: {
        filterMethod(keyword = '') {
          const { data, count, nameAlias } = this
          const result = keyword && data.filter(item => item[nameAlias].includes(keyword))
          if (result) {
            this.options = result.length > count ? result.slice(0, count) : result
          }
        }
      }
    }
    

完整代码

  • 模板代码

    <template>
      <el-select v-model="value" @change="handleChange" :filter-method="filterMethod" 
        filterable 
        clearable 
        multiple
        :placeholder="placeholder">
        <el-option 
          v-for="item of options" :key="item[idAlias]" 
          :label="item[nameAlias]" 
          :value="item[idAlias]">
        </el-option>
      </el-select>
    </template>
    
  • 脚本代码

    export default {
      model: {
        prop: 'value',
        event: 'change'
      },
      props: {
        data: {
          type: Array,
          required: true
        },
        value: {
          type: Array,
          required: true
        },
        count: {
          type: Number,
          default: 100
        },
        idAlias: {
          type: String,
          default: 'id'
        },
        nameAlias: {
          type: String,
          default: 'name'
        },
        placeholder: {
          type: String,
          default: '请选择'
        }
      },
      data() {
        return {
          options: []
        }
      },
      watch: {
        value: {
          handler(newVal, oldVal) {
            newVal && this.init()
          },
          immediate: true
        }
      },
      methods: {
        init() {
          const { data, count, idAlias, value: selectedIds } = this
          const allreadySelectedItems = data.filter(item => selectedIds.includes(item[idAlias]))
          const records = data.filter(item => !selectedIds.includes(item[idAlias])).slice(0, count - allreadySelectedItems.length)
          this.options = [...allreadySelectedItems, ...records]
        },
        handleChange(value) {
          this.$emit('change', value)
        },
        filterMethod(keyword = '') {
          const { data, count, nameAlias } = this
          const result = keyword && data.filter(item => item[nameAlias].includes(keyword))
          if (result) {
            this.options = result.length > count ? result.slice(0, count) : result
          }
        }
      }
    }
    

    一起学习,加群交流看 沸点