文章目录
- 功能介绍
- 一开始的代码
- 领导让我们分析一下
- 开始优化
- 如何监听事件和传参?
- 定位操作栏
- 更加优化
功能介绍
菜鸟最近做的一个功能如下:
后端返回两个很大的数组,例如:数组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
}