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

相关文章

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

day-51 合并零之间的节点

思路 直接遍历链表即可,遇到val=0跳过,val非零则加在一起,最后返回即可 解题过程 返回链表可以有头结点,方便插入,返回head.next Code /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}*

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟)

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟) 题目描述 给定一个链表,链表中的每个节点代表一个整数。链表中的整数由 0 分隔开,表示不同的区间。链表的开始和结束节点的值都为 0。任务是将每两个相邻的 0 之间的所有节点合并成一个节点,新节点的值为原区间内所有节点值的和。合并后,需要移除所有的 0,并返回修改后的链表头节点。 思路分析 初始化:创建一个虚拟头节点

vue2 组件通信

props + emits props:用于接收父组件传递给子组件的数据。可以定义期望从父组件接收的数据结构和类型。‘子组件不可更改该数据’emits:用于定义组件可以向父组件发出的事件。这允许父组件监听子组件的事件并作出响应。(比如数据更新) props检查属性 属性名类型描述默认值typeFunction指定 prop 应该是什么类型,如 String, Number, Boolean,

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

Solr 使用Facet分组过程中与分词的矛盾解决办法

对于一般查询而言  ,  分词和存储都是必要的  .  比如  CPU  类型  ”Intel  酷睿  2  双核  P7570”,  拆分成  ”Intel”,”  酷睿  ”,”P7570”  这样一些关键字并分别索引  ,  可能提供更好的搜索体验  .  但是如果将  CPU  作为 Facet  字段  ,  最好不进行分词  .  这样就造成了矛盾  ,  解决方法

ORACLE 11g 创建数据库时 Enterprise Manager配置失败的解决办法 无法打开OEM的解决办法

在win7 64位系统下安装oracle11g,在使用Database configuration Assistant创建数据库时,在创建到85%的时候报错,错误如下: 解决办法: 在listener.ora中增加对BlueAeri-PC或ip地址的侦听,具体步骤如下: 1.启动Net Manager,在“监听程序”--Listener下添加一个地址,主机名写计

Anaconda 中遇到CondaHTTPError: HTTP 404 NOT FOUND for url的问题及解决办法

最近在跑一个开源项目遇到了以下问题,查了很多资料都大(抄)同(来)小(抄)异(去)的,解决不了根本问题,费了很大的劲终于得以解决,记录如下: 1、问题及过程: (myenv) D:\Workspace\python\XXXXX>conda install python=3.6.13 Solving environment: done.....Proceed ([y]/n)? yDownloa