组织架构的列表页有关于公司人员架构的树形结构展示,某大客户有10万员工,造成组织架构的列表渲染卡顿,用户点击经常造成页面崩溃。
需求背景:左边是树形目录,多层级展示,层级结构未作限制。点击左边目录会展示对应的列表,点击右边对应用户的组织属性,也会联动左边的目录展示。
技术背景:vue2 + element el-tree
问题:1w数据页面展示无影响,当数据量达到5w+时页面开始卡顿甚至崩溃(导致公司某大客户持续投诉了一个月,他们有十几万员工)。
从技术角度来看,主要由以下原因造成:
1.虚拟dom的渲染开销
Vue 使用虚拟 DOM 来高效地更新真实 DOM。然而,虚拟 DOM 的创建和更新仍然需要一定的计算开销:
- 节点数量过多:1 万条数据意味着需要创建 1 万个虚拟 DOM 节点,这会占用大量内存和计算资源。
- 渲染时间过长:大量节点的渲染会导致主线程阻塞,页面出现卡顿。
2.响应式数据监听的开销
Vue 会对组件的 data 进行响应式处理,即递归地将数据属性转换为 getter/setter
3.组件生命周期钩子的执行
每个 el-tree 节点都是一个组件,Vue 会为每个组件执行生命周期钩子(如 created、mounted)
4.el-tree 的实现机制
el-tree会一次性渲染所有节点,并且为每个节点绑定事件(如点击、展开/折叠等)
综合以上原因,结合需求背景,分析得出以下几点:
- 页面左右两块区域是互相联动,所有必须数据节点必须全部渲染出来
- 目录层级未作限制。根据客户实际情况,battle产品,最终将最大层级限制到5层
- 接口直接返回嵌套的数据结构,对于深度较大的树,递归调用栈会占用大量内存。改成使用扁平数据结构,修改z-tree适配vue进行目录渲染。
show me the code
step 1:安装依赖, 当前ztree版本3.5.24
npm install ztree jquery
ztree初始化逻辑:
// 1. 引入样式和核心文件
import 'ztree/css/zTreeStyle/zTreeStyle.css'
import 'ztree'// 2. 定义容器
<div id="treeId" class="ztree"></div>// 3. 初始化实例(需在 DOM 加载完成后执行)
const setting = {}; // 配置项
const zNodes = []; // 数据
const zTreeObj = $.fn.zTree.init($('#treeId'), setting, zNodes);
step 2: 封装ztree组件,创建ZTree.vue文件
<template><div :id="treeId" class="ztree"></div>
</template><script>
import 'ztree/css/zTreeStyle/zTreeStyle.css'
import 'ztree'export default {name: 'ZTree',props: {data: Array, // 原始数据setting: Object // zTree 配置},data() {return {treeId: `ztree_${Math.random().toString(36).substr(2, 9)}`, // 唯一 IDzTreeObj: null // zTree 实例}},mounted() {this.initZTree(); // 初始化},methods: {initZTree() {this.zTreeObj = $.fn.zTree.init($(`#${this.treeId}`),this.setting,this.data);}}
}
</script>
通过 watch 监听数据变化,重新渲染:
watch: {data(newData) {if (this.zTreeObj) {this.zTreeObj.destroy(); // 销毁旧实例}this.initZTree(newData); // 重新初始化}
}
step3: 字段映射
接口返回数据字段如下
[{ id: 1, label: '根节点', parentId: null },{ id: 2, label: '子节点', parentId: 1 }
]
zTree 默认需要以下字段
[{ id: 1, name: '根节点', pId: null },{ id: 2, name: '子节点', pId: 1 }
]
在组件中新增 fieldMapping 属性,支持字段映射:
props: {fieldMapping: {type: Object,default: () => ({id: 'id',name: 'name',pId: 'pId'})}
}
通过计算属性 convertedData 转换数据:
computed: {convertedData() {return this.data.map(node => ({id: node[this.fieldMapping.id],name: node[this.fieldMapping.name],pId: node[this.fieldMapping.pId]}));}
}
ZTree.vue完整代码如下:
<template><div :id="treeId" class="ztree"></div>
</template><script>
import "ztree/css/zTreeStyle/zTreeStyle.css";
import "ztree";export default {name: "ZTree",props: {// 树数据data: {type: Array,default: () => [],},// 字段映射配置fieldMapping: {type: Object,default: () => ({id: "id", // 节点唯一标识字段name: "name", // 节点显示文本字段pId: "pId", // 父节点标识字段level: "level", // 层级字段(可选)}),},// zTree 配置setting: {type: Object,default: () => ({}),},},data() {return {treeId: `ztree_${Math.random().toString(36).substr(2, 9)}`, // 生成唯一 IDzTreeObj: null, // zTree 实例};},computed: {// 转换后的数据convertedData() {const mapping = this.fieldMapping;const data = this.data;// 一次遍历完成数据转换const result = data.map((node) => {const converted = {};Object.keys(mapping).forEach((key) => {const mapValue = mapping[key];converted[key] =typeof mapValue === "function"? mapValue(node) // 动态计算字段值: node[mapValue] ?? node[key]; // 直接映射字段值});return converted;});return result;},},watch: {// 监听数据变化,重新加载树convertedData: {handler(newData) {this.reloadTree(newData);},deep: true,},},mounted() {this.initZTree(); // 初始化 zTreeconsole.log("zTree 实例:", this.zTreeObj); // 打印 zTree 实例},methods: {// 初始化 zTreeinitZTree() {this.zTreeObj = window.$.fn.zTree.init(window.$(`#${this.treeId}`),this.setting,this.convertedData);},// 重新加载树reloadTree(data) {if (this.zTreeObj) {this.zTreeObj.destroy(); // 销毁旧实例}this.initZTree(data); // 初始化新实例},},
};
</script><style scoped>
.ztree {width: 100%;height: 100%;overflow: auto;
}
</style>
step4: 父组件调用ztree组件
启用扁平结构
setting: {data: {simpleData: {enable: true, // 启用扁平模式idKey: 'id', // 节点 ID 字段名pIdKey: 'pId' // 父节点 ID 字段名}}
}
父组件代码如下:
<template><div id="app"><ZTree :data="treeData" :fieldMapping="fieldMap" :setting="treeSetting" /></div>
</template><script>
import ZTree from "./components/ZTree.vue";
import { generateFlatTreeData } from "./utils";export default {components: {ZTree,},data() {return {// 接口返回的扁平结构数据treeData: [{ id: 1, label: "根节点", level: 1, parentId: null },{ id: 2, label: "子节点 1", level: 2, parentId: 1 },{ id: 3, label: "子节点 2", level: 2, parentId: 1 },],// 字段映射规则fieldMap: {id: "id",name: "label", // 将接口的 label 字段映射为 zTree 的 name 字段pId: "parentId", // 将接口的 parentId 字段映射为 zTree 的 pId 字段level: "level",},// zTree 配置treeSetting: {data: {simpleData: {enable: true, // 启用扁平结构},},view: {showIcon: true, // 显示图标},},};},mounted() {// 模拟异步请求const data = generateFlatTreeData(5, 100000);// const data = generateFlatTreeData(5, 800000);console.log("data.length", data);this.treeData = data;},
};
</script><style>
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;
}
</style>
项目完整代码见github: https://github.com/webLion200/vue-ztree