虚拟化表格(Virtualized Table)性能优化

文章目录

  • 功能介绍
  • 一开始的代码
  • 领导让我们分析一下
  • 开始优化
  • 如何监听事件和传参?
  • 定位操作栏
  • 更加优化

功能介绍

菜鸟最近做的一个功能如下:

后端返回两个很大的数组,例如:数组a 1w条,数组b 2w条,然后要操作b的数据去a里面,然后操作a的去b里面,最后把修改后的数组a和数组b返回给后端!且这个操作,是可以撤销的,用户操作了,但是没保存,是可以直接叉了,就不改后端数据!且数据还是可以搜索的!

一开始的代码

菜鸟一开始其实也考虑到了性能问题,但是当时是测试环境,最多就几十条数据,用 el-table 完全够用,且当时用 Virtualized Table 虚拟化表格 来渲染的时候老是 eslint 报错,所以当时就没管了!

接下来是老的代码

<script setup>
import { Search } from '@element-plus/icons-vue'

import { getExcelApi, saveReportInfoApi } from '@/network/analysisApi'

const props = defineProps({
  dialogVisible: {
    type: Boolean,
    default: false
  },
  id: {
    type: Number,
    default: -1
  }
})

const emit = defineEmits(['closeEvent'])

// 关闭弹窗
function handleClose() {
  emit('closeEvent', false)
}
const dialogBox = ref()
function closeDialog() {
  dialogBox.value.resetFields()
}

// 需要返回后端的数据
const subformData = {
  fileName: '',
  id: props.id,
  reportAPath: '',
  outputPath: ''
}
// 表数据
let reportA = ref([])
let reportB = ref([])
let oldreportA = []
let oldreportB = []
const loading = ref(true)
// 获取数据
getExcelApi(props.id)
  .then((res) => {
    console.log(res)
    if (res.code == 200) {
      subformData.fileName = res.data.fileName
      subformData.reportAPath = res.data.reportAPath
      subformData.outputPath = res.data.outputPath
      reportA.value = res.data.reportA
      reportB.value = res.data.reportB
      oldreportA = res.data.reportA
      oldreportB = res.data.reportB
      if (res.data.reportB.length <= 50 && res.data.reportA.length <= 50) {
        loading.value = false
      } else {
        setTimeout(() => {
          loading.value = false
        }, 5000)
      }
    } else {
      ElMessage({
        message: res.message,
        type: 'error'
      })
    }
  })
  .catch((err) => {
    console.log(err)
  })

// 搜索
let searchVal = ref('')
const search = () => {
  reportA.value = oldreportA.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
  reportB.value = oldreportB.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
}

// 表A减数据
const reduceFun = (e) => {
  let index = oldreportA.findIndex(
    (item) =>
      item.id === e.row.id &&
      item.barcode === e.row.barcode &&
      item.patientName === e.row.patientName &&
      item.species === e.row.species
  )
  let data = oldreportA.splice(index, 1)
  oldreportB.splice(0, 0, data[0])
  reportA.value = oldreportA.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
  reportB.value = oldreportB.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
}

// 表B加数据
const addFun = (e) => {
  let index = oldreportB.findIndex(
    (item) =>
      item.id === e.row.id &&
      item.barcode === e.row.barcode &&
      item.patientName === e.row.patientName &&
      item.species === e.row.species
  )
  let data = oldreportB.splice(index, 1)
  oldreportA.splice(0, 0, data[0])
  reportA.value = oldreportA.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
  reportB.value = oldreportB.filter(function (i) {
    return i.patientName.includes(searchVal.value)
  })
}

// 提交表单
const submit = () => {
  subformData.reportA = oldreportA
  subformData.reportB = oldreportB
  saveReportInfoApi(subformData)
    .then((res) => {
      console.log(res)
      if (res.code === 200) {
        ElMessage({
          message: '提交审核成功!',
          type: 'success'
        })
        handleClose()
      } else {
        ElMessage({
          message: res.message,
          type: 'error'
        })
      }
    })
    .catch((err) => {
      console.log(err)
    })
}

// 定义table的表头
const columns = [
  {
    title: '测序批次',
    dataKey: 'batch',
    width: 300
  },
  {
    title: 'barcode',
    dataKey: 'barcode',
    width: 100
  },
  {
    title: '患者姓名',
    dataKey: 'patientName',
    width: 100
  },
  {
    title: '体系',
    dataKey: 'structure',
    width: 350
  },
  {
    title: '样本编号',
    dataKey: 'sampleNum',
    width: 150
  },
  {
    title: '报告编号',
    dataKey: 'reportNum',
    width: 150
  },
  {
    title: '样本类型',
    dataKey: 'sampleType',
    width: 150
  },
  {
    title: '提取Reads总数',
    dataKey: 'extractReads',
    width: 140
  },
  {
    title: '样本比对总reads',
    dataKey: 'sampleContrastReads',
    width: 150
  },
  // 内参检出情况
  // 分类
  {
    title: 'Species',
    dataKey: 'species',
    width: 300
  },
  {
    title: '物种中文名',
    dataKey: 'speciesCn',
    width: 250
  },
  {
    title: '物种比对Reads数',
    dataKey: 'speciesContrastReads',
    width: 150
  },
  // 样本检出靶标数
  // 特异靶标数
  {
    title: '综合可信度',
    dataKey: 'credibility',
    width: 150
  },
  {
    title: 'DNC的Reads数',
    dataKey: 'dncReads',
    width: 150
  },
  {
    title: '样本质控总reads',
    dataKey: 'qualityReads',
    width: 150
  },
  // DNC的靶标数
  {
    title: '同批最大reads数',
    dataKey: 'maxReads',
    width: 150
  },
  // 同批最高bc
  // 同批最高核酸编号
  {
    title: '物种类别',
    dataKey: 'speciesCategory',
    width: 150
  },
  {
    title: '定植情况',
    dataKey: 'planting',
    width: 400
  },
  {
    title: '结果解释',
    dataKey: 'resultExplain',
    width: 800
  },
  {
    title: '物种所在盘',
    dataKey: 'speciesDisk',
    width: 250
  },
  // 表中没有可能要修改
  {
    title: 'Genus',
    dataKey: 'genus',
    width: 150
  },
  {
    title: '属名',
    dataKey: 'genericName',
    width: 150
  },
  {
    title: '危害程度分类',
    dataKey: 'harm',
    width: 150
  },
  {
    title: '检出数/10000',
    dataKey: 'detectionNumber',
    width: 150
  },
  {
    title: '单样本Score',
    dataKey: 'sampleScore',
    width: 150
  }
]
</script>

<template>
  <div>
    <el-dialog
      title="结果筛选"
      ref="dialogBox"
      :modelValue="dialogVisible"
      :before-close="handleClose"
      @close="closeDialog"
      width="90%"
      top="30px"
      :close-on-click-modal="false"
      :destroy-on-close="true"
    >
      <div style="display: flex; width: 300px">
        <el-input v-model="searchVal" placeholder="患者姓名" clearable></el-input>
        <el-button style="margin-left: 50px" type="primary" :icon="Search" @click="search"
          >搜索</el-button
        >
      </div>
      <hr />
      <p>表格A</p>
      <div style="height: 300px">
        <el-table
          v-loading="loading"
          element-loading-text="加载中..."
          :data="reportA"
          style="width: 100%; height: 100%"
        >
          <template v-for="(item, index) in columns" :key="index">
            <el-table-column :prop="item.dataKey" :label="item.title" :width="item.width" />
          </template>
          <el-table-column fixed="right" label="操作" width="80" center>
            <template #default="scope">
              <el-button type="primary" size="small" @click="reduceFun(scope)"> - </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <hr />
      <p>表格B</p>
      <div style="height: 300px">
        <el-table
          v-loading="loading"
          element-loading-text="加载中..."
          :data="reportB"
          style="width: 100%; height: 100%"
        >
          <template v-for="(item, index) in columns" :key="index">
            <el-table-column :prop="item.dataKey" :label="item.title" :width="item.width" />
          </template>
          <el-table-column fixed="right" label="操作" width="80" center>
            <template #default="scope">
              <el-button type="primary" size="small" @click="addFun(scope)"> + </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <template #footer>
        <div>
          <el-button type="primary" @click="submit">提交</el-button>
          <el-button @click="handleClose">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

但是这段代码在生产环境中就完全不够看了,生产环境不管是reportA还是reportB都是几千条左右,即使1秒就获取到了后端数据,但是 el-table 加载就要几秒钟,所以菜鸟直接写了一个5秒的定时器,等5秒后差不多渲染完了才把蒙层关闭(有点掩耳盗铃的感觉)!

重点是当菜鸟滚动列表的时候,那叫一个卡顿,且如果进行了移动数据的操作,那又会一卡一卡的,如果加上搜索,卡顿得让人难以想象!

领导让我们分析一下

卡成这样,用户肯定是受不了,所以领导就找我们分析原因!

菜鸟感觉前端数据量有点大,不如:用分页搜索并配合后端一起解决!但是很快被后端否决了,因为很麻烦,例如:

a查了10条,b查了10条,操作了b的一条去了a,那a点击第二页应该就是9-19条,而不是之前的10-20条,b也会变成1-11条(去掉操作的那一条),而不是1-10条了!

每一个操作都要向后端去请求,并告诉后端,数组a增加了哪一个、减少了哪一个、数组b增加了哪一个、减少了哪一个,交给后端去处理分页(并不改数组库)!

显然上面的这个思路得上千万条数据可能会使用的,菜鸟这个还不至于!

所以将思路定为前端性能优化,领导直接发话:这优化不了就该优化菜鸟我了!
在这里插入图片描述

开始优化

既然分到自己头上了,那就只能干了!奥里给!

菜鸟想起来了之前的 Virtualized Table 虚拟化表格 ,当时使用确实会报错,但是把那个报错的代码删除,确实反应很快,只是当时没有深究如何解决报错,想着偷懒去了,现在就硬上了!

所以代码改成了这样:

<script setup>
import { Search } from '@element-plus/icons-vue'

import { getExcelApi, saveReportInfoApi } from '@/network/analysisApi'

const props = defineProps({
  dialogVisible: {
    type: Boolean,
    default: false
  },
  id: {
    type: Number,
    default: -1
  }
上一篇:JVM类加载过程-Loading


下一篇:哈佛商业评论 | 生成式人工智能:仍然是预测机器