文章目录
- 功能介绍
- 一开始的代码
- 领导让我们分析一下
- 开始优化
- 如何监听事件和传参?
- 定位操作栏
- 更加优化
功能介绍
菜鸟最近做的一个功能如下:
后端返回两个很大的数组,例如:数组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.fileNamesubformData.reportAPath = res.data.reportAPathsubformData.outputPath = res.data.outputPathreportA.value = res.data.reportAreportB.value = res.data.reportBoldreportA = res.data.reportAoldreportB = res.data.reportBif (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 = oldreportAsubformData.reportB = oldreportBsaveReportInfoApi(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-dialogtitle="结果筛选"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-tablev-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-tablev-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}
})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([])
// 保存表头 --》 操作columns
let reportATitle = []
let reportBTitle = []
const columnsA = ref([])
const columnsB = 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.fileNamesubformData.reportAPath = res.data.reportAPathsubformData.outputPath = res.data.outputPathreportA.value = res.data.reportAconst tempA = reportA.value[0]for (let i in tempA) {console.log(i)if (i == 'id') continuereportATitle.push(i)}for (let i in reportATitle) {columnsA.value.push({title: reportATitle[i],key: reportATitle[i],dataKey: reportATitle[i],width: reportATitle[i] === '结果解释' || reportATitle[i] === '描述' ? 1200 : 200})}columnsA.value.push({key: 'operations',title: '操作',cellRenderer: () => (<><ElButton type="primary" size="small">-</ElButton></>),width: 80,align: 'center'})reportB.value = res.data.reportBconst tempB = reportB.value[0]for (let i in tempB) {if (i == 'id') continuereportBTitle.push(i)}for (let i in reportBTitle) {columnsB.value.push({title: reportBTitle[i],key: reportBTitle[i],dataKey: reportBTitle[i],width: reportBTitle[i] === '结果解释' || reportBTitle[i] === '描述' ? 1200 : 200})}columnsB.value.push({key: 'operations',title: '操作',cellRenderer: () => (<><ElButton type="primary" size="small">+</ElButton></>),width: 80,align: 'center'})oldreportA = res.data.reportAoldreportB = res.data.reportBloading.value = false} 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['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}const uniqueItem = ['id', 'Barcode', '患者姓名', 'Species']
// 表A减数据
const reduceFun = (e) => {console.log(e)let index = oldreportA.findIndex((item) => {console.log(item)let num = 0for (let i of uniqueItem) {if (item[i] === e[i]) {num++}}return num === 4})let data = oldreportA.splice(index, 1)oldreportB.splice(0, 0, data[0])reportA.value = oldreportA.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}// 表B加数据
const addFun = (e) => {console.log(e)let index = oldreportB.findIndex((item) => {console.log(item)let num = 0for (let i of uniqueItem) {if (item[i] === e[i]) {num++}}return num === 4})let data = oldreportB.splice(index, 1)oldreportA.splice(0, 0, data[0])reportA.value = oldreportA.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}// 提交表单
const submit = () => {subformData.reportA = oldreportAsubformData.reportB = oldreportBsaveReportInfoApi(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)})
}
</script><template><div><el-dialogtitle="结果筛选"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-auto-resizer><template #default="{ height, width }"><el-table-v2 :columns="columnsA" :data="reportA" :width="width" :height="height" fixed><template #overlay v-if="loading"><divclass="el-loading-mask"style="display: flex; align-items: center; justify-content: center"><el-icon class="is-loading"><i-ep-loading /></el-icon></div></template></el-table-v2></template></el-auto-resizer></div><hr /><p>表格B</p><div style="height: 300px"><el-auto-resizer><template #default="{ height, width }"><el-table-v2 :columns="columnsB" :data="reportB" :width="width" :height="height" fixed><template #overlay v-if="loading"><divclass="el-loading-mask"style="display: flex; align-items: center; justify-content: center"><el-icon class="is-loading"><i-ep-loading /></el-icon></div></template></el-table-v2></template></el-auto-resizer></div><template #footer><div><el-button type="primary" @click="submit">提交</el-button><el-button @click="handleClose">关闭</el-button></div></template></el-dialog></div>
</template>
果然不出所料,还是报错:
Parsing error: Unexpected token <
这里菜鸟直接反手 ChatGPT,问了一个:
然后菜鸟就知道了,原来是jsx搞的鬼,知道了原因,解决就很快了,直接反手再来一发 ChatGPT :
然后配置一下 .eslintrc.cjs
配置好了之后发现没有报错,项目可以运行,自信满满点到这个界面,发现还是报错:
问了ChatGPT 发现是要在script
标签上加个lang="jsx"
:
完美解决!
如何监听事件和传参?
这样渲染倒是上去了,但是jsx菜鸟不会呀,咋监听按钮的点击事件?咋传参数?
菜鸟只能一个一个尝试了!菜鸟想着都是 element 应该传值是一样的吧,所以变成了这样!
columnsA.value.push({key: 'operations',title: '操作',cellRenderer: (row) => (<><ElButton type="primary" size="small" @click="addFun(row)">-</ElButton></>),width: 80,align: 'center'
})
但是直接报错
继续问 GPT:
然后菜鸟写成了这样
columnsA.value.push({key: 'operations',title: '操作',cellRenderer: (row) => (<><ElButton type="primary" size="small" onClick="addFun(row)">-</ElButton></>),width: 80,align: 'center'
})
发现还是没有用,但是不报错了,果断 GPT:
最后代码成型:
<script lang="jsx" 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([])
// 保存表头 --》 操作columns
let reportATitle = []
let reportBTitle = []
const columnsA = ref([])
const columnsB = 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.fileNamesubformData.reportAPath = res.data.reportAPathsubformData.outputPath = res.data.outputPathreportA.value = res.data.reportAconst tempA = reportA.value[0]for (let i in tempA) {console.log(i)if (i == 'id') continuereportATitle.push(i)}for (let i in reportATitle) {columnsA.value.push({title: reportATitle[i],key: reportATitle[i],dataKey: reportATitle[i],width: reportATitle[i] === '结果解释' || reportATitle[i] === '描述' ? 1200 : 200})}columnsA.value.push({key: 'operations',title: '操作',cellRenderer: (row) => (<><ElButton type="primary" size="small" onClick={() => reduceFun(row.rowData)}>-</ElButton></>),width: 80,align: 'center'})reportB.value = res.data.reportBconst tempB = reportB.value[0]for (let i in tempB) {if (i == 'id') continuereportBTitle.push(i)}for (let i in reportBTitle) {columnsB.value.push({title: reportBTitle[i],key: reportBTitle[i],dataKey: reportBTitle[i],width: reportBTitle[i] === '结果解释' || reportBTitle[i] === '描述' ? 1200 : 200})}columnsB.value.push({key: 'operations',title: '操作',cellRenderer: (row) => (<><ElButton type="primary" size="small" onClick={() => addFun(row.rowData)}>+</ElButton></>),width: 80,align: 'center'})oldreportA = res.data.reportAoldreportB = res.data.reportBloading.value = false} 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['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}const uniqueItem = ['id', 'Barcode', '患者姓名', 'Species']
// 表A减数据
const reduceFun = (e) => {console.log(e)let index = oldreportA.findIndex((item) => {console.log(item)let num = 0for (let i of uniqueItem) {if (item[i] === e[i]) {num++}}return num === 4})let data = oldreportA.splice(index, 1)oldreportB.splice(0, 0, data[0])reportA.value = oldreportA.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}// 表B加数据
const addFun = (e) => {console.log(e)let index = oldreportB.findIndex((item) => {console.log(item)let num = 0for (let i of uniqueItem) {if (item[i] === e[i]) {num++}}return num === 4})let data = oldreportB.splice(index, 1)oldreportA.splice(0, 0, data[0])reportA.value = oldreportA.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})reportB.value = oldreportB.filter(function (i) {return i['患者姓名'].includes(searchVal.value)})
}// 提交表单
const submit = () => {subformData.reportA = oldreportAsubformData.reportB = oldreportBsaveReportInfoApi(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)})
}
</script><template><div><el-dialogtitle="结果筛选"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-auto-resizer><template #default="{ height, width }"><el-table-v2 :columns="columnsA" :data="reportA" :width="width" :height="height" fixed><template #overlay v-if="loading"><divclass="el-loading-mask"style="display: flex; align-items: center; justify-content: center"><el-icon class="is-loading"><i-ep-loading /></el-icon></div></template></el-table-v2></template></el-auto-resizer></div><hr /><p>表格B</p><div style="height: 300px"><el-auto-resizer><template #default="{ height, width }"><el-table-v2 :columns="columnsB" :data="reportB" :width="width" :height="height" fixed><template #overlay v-if="loading"><divclass="el-loading-mask"style="display: flex; align-items: center; justify-content: center"><el-icon class="is-loading"><i-ep-loading /></el-icon></div></template></el-table-v2></template></el-auto-resizer></div><template #footer><div><el-button type="primary" @click="submit">提交</el-button><el-button @click="handleClose">关闭</el-button></div></template></el-dialog></div>
</template>
定位操作栏
但是这样了,菜鸟还是不满足,感觉一般这个操作都是要固定起来,而不是在最后一行看不见!
然后菜鸟又踩坑了,发现就是定位不了,最后对比官方网站找到原因,发现是需要从 element plus 里面引入 TableV2FixedDir
简化后的代码 (避免大家看很多,提出来这个部分了):
import { TableV2FixedDir } from 'element-plus'columnsB.value.push({key: 'operations',title: '操作',cellRenderer: (row) => (<><ElButton type="primary" size="small" onClick={() => addFun(row.rowData)}>+</ElButton></>),width: 80,align: 'center',fixed: TableV2FixedDir.RIGHT
})
更加优化
这个是菜鸟沸点底下的大佬给的思路:
所以这里把ref 换成 shallowRef
let reportA = shallowRef([])
let reportB = shallowRef([])
到此性能优化完成,可能对大佬来说真的很简单,但是菜鸟还是感觉有点成就感,毕竟性能提升了50倍以上!