el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法

本文主要是介绍el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

解决el-tree组件展示节点过多时造成页面卡顿、奔溃

前几天测试提了个BUG,文件列表展示5w个文件页面会卡顿甚至奔溃。
项目用的是vue+element-ui框架,我是使用el-tree进行渲染文件列表的。
参考网上使用virtual-scroll-list插件与el-tree源码写成一个新组件。virtual-scroll-list可以只渲染页面呈现部分的节点,这样就不会造成卡顿了,源el-tree是直接将5w个节点直接渲染到页面,导致页面奔溃。
这是使用virtual-scroll-list插件与el-tree源码结合后的组件:github组件下载 、gitee组件下载
组件使用方法(传入的属性)与el-tree一致,可根据自己的业务需求更改,我做的需求只是进行文件导出。
组件使用示例:

<virtualNodeTreeref="dirTree":data.sync="treeData":load="loadDir":keeps="50":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-key="path"class="treeWrap"@check-change="handleCheckChange"><span slot-scope="{ data }"><svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/><span v-else><svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/></span><span>{{ data.fname }}</span></span></virtualNodeTree>

组件引入:
在这里插入图片描述

效果图:
请添加图片描述

more文件下面有5w个文件,实际页面渲染50个文件,根据组件传入的keeps展示文件数,默认30个;
注意:
1.搜索只能搜到已渲染的节点,可以自己做递归搜索源数据,不过这样的话数据一多会很卡,建议后端写个搜索api
2.该组件的父容器一定要确定高度,不能以整个body作为父容器,这样有可能滚动时渲染不出下面的文件。

补充:
我使用的完整代码

<template><div class="app-container"><div><el-buttonclass="ame-button"size="mini"type="primary"@click="exportHandle":loading="exportLoading":disabled="exportLoading">导出</el-button></div><virtualNodeTreev-loading="loading"ref="dirTree":data.sync="treeData":load="loadDir":keeps="50":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-key="path"class="treeWrap"@check="handleNodeCheck"><span slot-scope="{ data }"><svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/><span v-else><svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/></span><span>{{ data.fname }}</span></span></virtualNodeTree><!--<el-treeref="dirTree":data.sync="treeData":load="loadDir":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-key="path"class="treeWrap"@check-change="handleCheckChange"><span slot-scope="{ data }"><svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/><span v-else><svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/></span><span>{{ data.fname }}</span></span></el-tree>--></div>
</template><script>
import { slotFileList, slotFileExport } from '@/api/disc.js'
import { downloadFile } from '@/utils/index'
import getFileIcon from '@/utils/getFileIcon'
import { getFilesNumFromFolder } from '@/api/mtoptical'
import { Message } from 'element-ui'
import virtualNodeTree from '@/components/virtualNodeTree/tree'export default {name: 'DiscFileDetail',components: { virtualNodeTree },data() {return {treeData: [],dirInfos: [],lock: false,loading:false,exportLoading:false,exportMaxNum: 10000 // 允许导出文件的最大数量}},mounted() {},methods: {async handleNodeCheck(data, selctedInfo) {const checked = selctedInfo.checkedKeys.includes(data.path)if (data.ftype == '1' && checked) {if (typeof data.allFileNum === 'number') {// 已获悉该文件夹数量的不再查询this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: parseInt(data.allFileNum), checked })return}const params = {nodeId: this.$route.query.nodeId || '',libId: this.$route.query.libId || '',grooveId: this.$route.query.grooveId || '',path: data.path || '/'}if (this.$route.query.rfid) params.rfid = this.$route.query.rfidconst res = await this.getFilesNumFromFolder(params)data.allFileNum = resthis.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data: data || '/', fileNum: parseInt(res), checked })// if (res > this.exportMaxNum) {//   this.$message.error(`导出文件数量不能超过${this.exportMaxNum}`)//   // 取消勾选//   this.$refs.dirTree.setChecked(data, false, true)// } else {//   data.allFileNum = res//   this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data: data || '/', fileNum: parseInt(res), checked })//   console.log('添加数量完成', this.dirInfos[0]);// }} else if (data.ftype != '1' && checked) {// 勾选文件this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })} else if (data.ftype != '1') {// 取消勾选文件this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })} else {// 取消勾选文件夹this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })}},getFilesNumFromFolder(params) {return new Promise((resolve, reject) => {const loaderTip = this.$loading({lock: true,text: '请稍等......',spinner: 'el-icon-loading',background: 'rgba(0, 0, 0, 0.7)'})getFilesNumFromFolder(params).then(res => {// console.log('获取到的文件数量', res);loaderTip.close()resolve(typeof res === 'number' ? parseInt(res) : 0)}).catch(() => {loaderTip.close()reject(0)})})},fileInputHandle(file) {const nameSplit = file.fname.split('.')let iconInfo = nullif (nameSplit.length > 1) {iconInfo = getFileIcon(nameSplit[nameSplit.length - 1])} else {iconInfo = getFileIcon('其他')}return iconInfo},async exportUtils(fn, params) {return new Promise(async(resolve, reject) => {let fileTotal, dataPathswitch (fn) {// 添加文件夹信息到文件夹信息集合内case 'addDirInfo':for (let i = 0; i < params.dirInfos.length; i++) {if (params.node.data.path === params.dirInfos[i].path) {params.dirInfos[i].children = params.addInforesolve('finished')}if (params.dirInfos[i].children) {const status = this.exportUtils('addDirInfo', { ...params, dirInfos: params.dirInfos[i].children })if (status === 'finished') resolve()}}break// 添加文件夹数量到文件夹信息集合内case 'fileNumChange':// 勾选的是文件比对时需要删除path后面的文件名后再比对if (params.data.ftype != '1') {const tmp = params.data.path.split('/')dataPath = tmp.slice(0, tmp.length - 1).join('/')} else {dataPath = params.data.path || '/'}for (let i = 0; i < params.dirInfos.length; i++) {// 找到该文件夹信息if (dataPath === params.dirInfos[i].path) {// console.log('已找到该信息', params);if (params.checked) {const curDirSelFileNum = params.data.ftype == '1' ? (typeof params.dirInfos[i].curSelFileNum === 'number' ? params.dirInfos[i].curSelFileNum : 0) : 0const curAllCheckedFileNum = await this.exportUtils('getFileTotal') + (params.fileNum || 1) - curDirSelFileNum// 添加前检查是否超出最大导出数量if (curAllCheckedFileNum > this.exportMaxNum) {this.$message.error(`导出文件数量不能超过${this.exportMaxNum}`)// 取消勾选this.$refs.dirTree.setChecked(params.data, false, true)if (params.data.ftype == '1') {params.dirInfos[i].curSelFileNum = 0}resolve(params.data.ftype == '1' ? -curDirSelFileNum : 0)return}// console.log('当前勾选文件数', curAllCheckedFileNum);// 勾选的是文件夹if (params.data.ftype == '1') {params.dirInfos[i].allFileNum = params.fileNumparams.dirInfos[i].curSelFileNum = params.fileNumthis.exportUtils('setAllChildrenChecked', { children: params.dirInfos[i].children })resolve(params.fileNum - curDirSelFileNum)} else {if (params.dirInfos[i].curSelFileNum === 'unknown') {params.dirInfos[i].curSelFileNum = 1} else {params.dirInfos[i].curSelFileNum += 1}resolve(1)}} else {// 取消勾选if (params.data.ftype == '1') {params.dirInfos[i].curSelFileNum = 0if (params.dirInfos[i].allFileNum === 'unknown') {const queryParams = {nodeId: this.$route.query.nodeId || '',libId: this.$route.query.libId || '',grooveId: this.$route.query.grooveId || '',path: params.data.path || '/'}resolve(await this.getFilesNumFromFolder(queryParams))}resolve(params.dirInfos[i].allFileNum)} else {if (params.dirInfos[i].curSelFileNum !== 'unknown') {params.dirInfos[i].curSelFileNum -= 1}resolve(1)}}}if (params.dirInfos[i].children && params.dirInfos[i].children.length !== 0) {const num = await this.exportUtils('fileNumChange', { ...params, dirInfos: params.dirInfos[i].children })if (typeof num === 'number') {if (params.dirInfos[i].curSelFileNum === 'unknown') {params.dirInfos[i].curSelFileNum = num} else {params.dirInfos[i].curSelFileNum += params.checked ? num : -num}resolve(num)}}if (i === params.dirInfos.length - 1) resolve('continue')}break// 获取已勾选的文件总数case 'getFileTotal':fileTotal = this.dirInfos.reduce((total, item) => {const tmp = item.curSelFileNum === 'unknown' ? 0 : item.curSelFileNumreturn total + tmp}, 0)resolve(fileTotal)break// 当父文件夹勾选后,将所有已知文件总数量的子文件夹的curSelFileNum设置为allSelFileNumcase 'setAllChildrenChecked':if (Array.isArray(params.children) && params.children.length !== 0) {for (const i in params.children) {const allFileNum = params.children[i].allFileNumparams.children[i].curSelFileNum = allFileNum === 'unknown' ? 'unknown' : allFileNumif (params.children[i].children && params.children[i].children.length !== 0) {this.exportUtils('setAllChildrenChecked', params.children[i].children)}}}break}})},loadDir(node, resolve) {const temp = {nodeId: this.$route.query.nodeId || '',libId: this.$route.query.libId || '',oid: this.$route.query.oid || '',path: node.data.path || '/'}if (this.$route.query.src === 'warehouseTask' &&this.$route.query.rfid) {temp.rfid = this.$route.query.rfid} else {temp.grooveId = this.$route.query.grooveId || ''}this.loading = true;slotFileList(temp).then(res => {this.loading = false;if (res && res instanceof Array) {const addInfo = []for (const i in res) {if (res[i].ftype == '1') {addInfo.push({path: (node.data.path || '') + '/' + res[i].fname,level: node.level + 1,allFileNum: 'unknown',curSelFileNum: 'unknown',children: null})}}if (this.dirInfos.length === 0) {this.dirInfos.push({path: node.data.path || '/',allFileNum: 'unknown',curSelFileNum: 'unknown',level: node.level,children: addInfo})} else {this.exportUtils('addDirInfo', { dirInfos: this.dirInfos, node, addInfo })}const list = res.map(item => {return {...item,path: (node.data.path || '') + '/' + item.fname,leaf: item.ftype != '1'// disabled: item.ftype == '1'}})resolve(list)} else {resolve([])}}).catch(() => {this.loading = false;resolve([])})},exportHandle() {const checkeNodes = this.$refs.dirTree.getCheckedNodes()if (checkeNodes.length < 1) {this.$message.error('请选择需要导出的数据')return}this.exportLoading = true;setTimeout(() => {try {// const paths = checkeNodes.map(item => {//   return { fileName: item.path, fileType: item.ftype === '1' ? '1' : '2' }// })let paths = checkeNodes.map(item => {return { pathArr: item.path.split('/'), fileType: item.ftype === '1' ? '1' : '2' }})// 如果文件夹与子文件件都勾选了,只保留顶级文件夹for (let i = 0; i < paths.length; i++) {if (!paths[i]) continueout: for (let j = i + 1; j < paths.length; j++) {if (!paths[j]) continue outfor (let k = 0; k < paths[i].pathArr.length; k++) {if (paths[i].pathArr[k] !== paths[j].pathArr[k]) {continue out}}paths[j] = null}}// 过滤掉null的元素paths = paths.filter(item => item)// const paths = checkeNodes.map(item => item.path)const temp = {nodeId: this.$route.query.nodeId || '',libId: this.$route.query.libId || '',grooveId: this.$route.query.grooveId || '',oid: this.$route.query.oid || '',exportFiles: paths.map(item => {return { fileName: item.pathArr.join('/'), fileType: item.fileType === '1' ? '1' : '2' }})}if (this.$route.query.rfid) temp.rfid = this.$route.query.rfid// console.log('提交参数', temp);// returnslotFileExport(temp).then(res => {downloadFile(res, '导出', 'xlsx')this.exportLoading = false;}).catch(error => {const fileReader = new FileReader()fileReader.onload = function(e) {Message.error(this.result)}fileReader.readAsText(error.response.data)this.exportLoading = false;})} catch (e) {this.$message.error(e)this.exportLoading = false;}}, 20)}}
}
</script><style lang="scss" scoped>.el-tree {height: calc(100vh - 136px - 90px);}.app-container {background: #fff;padding: 20px;margin: 0px 10px 0 10px;/* height: 800px; */overflow-y: auto;border-radius: 5px;}.treeWrap {margin-top: 20px;border-radius: 4px;border: 1px solid #9e9e9e;}
</style>

这篇关于el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/280178

相关文章

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

vue解决子组件样式覆盖问题scoped deep

《vue解决子组件样式覆盖问题scopeddeep》文章主要介绍了在Vue项目中处理全局样式和局部样式的方法,包括使用scoped属性和深度选择器(/deep/)来覆盖子组件的样式,作者建议所有组件... 目录前言scoped分析deep分析使用总结所有组件必须加scoped父组件覆盖子组件使用deep前言

基于Qt Qml实现时间轴组件

《基于QtQml实现时间轴组件》时间轴组件是现代用户界面中常见的元素,用于按时间顺序展示事件,本文主要为大家详细介绍了如何使用Qml实现一个简单的时间轴组件,需要的可以参考下... 目录写在前面效果图组件概述实现细节1. 组件结构2. 属性定义3. 数据模型4. 事件项的添加和排序5. 事件项的渲染如何使用

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

使用JavaScript将PDF页面中的标注扁平化的操作指南

《使用JavaScript将PDF页面中的标注扁平化的操作指南》扁平化(flatten)操作可以将标注作为矢量图形包含在PDF页面的内容中,使其不可编辑,DynamsoftDocumentViewer... 目录使用Dynamsoft Document Viewer打开一个PDF文件并启用标注添加功能扁平化

SpringBoot如何访问jsp页面

《SpringBoot如何访问jsp页面》本文介绍了如何在SpringBoot项目中进行Web开发,包括创建项目、配置文件、添加依赖、控制层修改、测试效果以及在IDEA中进行配置的详细步骤... 目录SpringBoot如何访问JSP页python面简介实现步骤1. 首先创建的项目一定要是web项目2. 在

SQL Server数据库磁盘满了的解决办法

《SQLServer数据库磁盘满了的解决办法》系统再正常运行,我还在操作中,突然发现接口报错,后续所有接口都报错了,一查日志发现说是数据库磁盘满了,所以本文记录了SQLServer数据库磁盘满了的解... 目录问题解决方法删除数据库日志设置数据库日志大小问题今http://www.chinasem.cn天发

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca