前言
- LazyTree 组件是对于 FES 里面 Tree 组件的一个补充。开始之前先推荐一下我司的 FES,一个优秀的管理台应用解决方案,类似于 vue-element-admin ,但是比它更好用,感兴趣的朋友可以点击 传送门 去看一下,确实很节省开发时间。
- 目前 LazyTree 组件已经被 fes2.0 的公司内部内版本收录。
- 取名为 LazyTree 的原因是 Tree 组件名称已经被占用了。
gitee 地址
- gitee项目链接 gitee.com/jiangxiZhan…
- lazyTree 文件目录地址:gitee.com/jiangxiZhan…
主要文件
lazyTree.vue 入口根节点,处理主要逻辑
lazyTreeNode.vue 叶子节点,处理选中等事件
封装 LazyTree 组件的背景
为什么我要封装 LazyTree 组件
- 接手了公司的一个老项目,发现里面有些地方用到树形结构,但是里面的树形结构并不是用的公司共用组件库里面的 Tree 组件,而是用了一个叫 liquor-tree 的插件。观察了一下,发现原来是因为 Tree 组件不支持过滤节点的功能。
- 项目上有需求用树形结构展示选择公司的人员信息。公司大约有7000人左右,包含部门和科室信息的话总共是大约是7500条数据,使用 liquor-tree 全选整个公司组织架构的时候照成卡顿,卡顿时间 2s到3s,确实无法接受,我接手以后,业务都次提到想要优化该功能。
- 尝试过用自己过滤数据,传入 Tree 组件,实现过滤功能,发现效果不满意,全选依旧有卡顿的问题。
Tree 组件和 liquor-tree 数据量大卡顿的原因
- Tree 组件里面直接把折叠元素也渲染出来了,只是简单做了 display: none。这样的话就算不做其他操作,单单是渲染近万个节点就已经是很大的负担。一旦全选节点再去递归调用 setChild 事件,对内存的负担很大,也会导致卡顿。
- 当触发选择的时候 liquor-tree 组件去做一个递归去触发 checked 事件,递归过程中会先去寻找节点,然后触发点击事件,这样成本是高昂的。代码如下
check() {
if (this.node.checked()) {
this.node.uncheck()
} else {
this.node.check()
}
}
贴一下全选时候chrome Performance 中的截图,可以看到卡顿的原因确实是 checked 调用引起的,这里 checked 执行的时间达到了惊人的 65000ms
当然,其实这个递归调用的问题在Tree组件中一样也是存在的
实现的过程
分析怎么写出性能好的树形组件
- 惰性加载,当一个节点没有展开的时候我们无需去加载它的子节点,只有展开的时候才去加载。
- 目前组件的卡顿都是很大原因是递归调用了每个节点的点击事件,有多少个节点就需要调用多少次函数,而去调用函数的性能其实并不高。那我想我们只需要规避调如何复杂的递归调动就行。递归调用 checked 的原因是要更新每一个节点的被选中的状态,其实我们只需要把所有选中和未选中的状态记录在 data 中就可以,每次点击一个点击的时候去更新data中的数据就可以达到目的。
先写个文档,看需要哪些内容
代码实现
处理用户传入的 data 信息
- 用户传入的data信息不能直接使用。需要做一些简单的处理,当用户用户传入的 data 信息或者 defaultSelectedIds 改变的时候就需要处理data信息,把处理后的数据设置为 list。
- 这里的话我们主要是处理选中状态,我们转化一下,selected 分为3中状态分别是0:未选中,1:半选,2:选中。半选就是说子节点有部门选中,父节点就会进入半选状态。
- 主要代码如图
lazyTree.vue
transformData(list, level = 1, parentSelected = 0, parent = null) { // 初始化原始数据
return list.map((item) => {
const {
id, name, expand = false, data = null, children = []
} = item;
let { selected = 0 } = item;
if (this.multiple === false && this.defaultSelectedIds.length === 0 && selected === true) {
this.selectedId = id;
}
if (selected === true) {
selected = 2;
}
selected = this.selectedIdsMap[id] ? 2 : selected;
if (parentSelected === 2) {
selected = 2;
}
const obj = {
id, name, expand, data, level, isLeaf: children.length === 0, selected, isShow: true, parent
}
const newChildren = this.transformData(children, level + 1, selected, obj);
obj.children = newChildren;
return obj;
});
}
更新选中信息
- 这里的选中信息主要是靠数据进行驱动,主要表现在于所有非叶子节点的选中状态都是由其叶子节点决定,这样当选中叶子节点的时候我们先更新它的selected状态为2,然后去计算其父节点的选中状态。父节点的选中状态只需要由他的直系子节点决定。
- 更新子节点主要代码
lazyTreeNode.vue
updateChildrenSelected(node, selected) {
node.forEach((item) => {
const { children } = item;
item.selected = selected;
this.updateChildrenSelected(children, selected);
});
}
- 更新父节点选中状态主要代码
lazyTree.vue
updateParentSelected(node) {
if (!node || !node.parent) {
return;
}
let allSelectCount = 0;
let someSelectCount = 0;
const children = node.parent.children;
children.forEach((item) => {
const { selected } = item;
if (selected === 1) {
someSelectCount += 1;
} else if (selected === 2) {
allSelectCount += 1;
}
});
if (someSelectCount === 0 && allSelectCount === 0) {
node.parent.selected = 0;
} else if (someSelectCount > 0) {
node.parent.selected = 1;
} else if (allSelectCount > 0 && allSelectCount < children.length) {
node.parent.selected = 1;
} else if (allSelectCount === children.length) {
node.parent.selected = 2;
}
this.updateParentSelected(node.parent);
}
过滤节点功能
- 这里允许用户传入 filterText 字段进行过滤节点,我们只需要给一个 isShow 字段进行控制即可。当然防抖也是必不可少的。
- 主要代码如下
lazyTree.vue
防抖处理
watch: {
filterText: {
immediate: true,
handler(text) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.filterLabel = text;
this.filterTree(this.list, this.filterLabel.toLowerCase());
this.list.forEach((node) => {
this.updateSelected(node);
});
}, 300);
}
}
}
过滤节点
filterTree(list, filterLabel, show = false) { // 根据 name 字段过滤信息
if (!list || list.length === 0) {
return;
}
list.forEach((item) => {
const { name, children = [] } = item;
if (show === true || filterLabel.length === 0 || name.toLowerCase().indexOf(filterLabel) > -1) {
item.isShow = true;
this.filterTree(children, filterLabel, true);
} else {
item.isShow = this.filterTree(children, filterLabel, false);
}
});
return list.some(item => item.isShow)
}
性能对比
在数据量相同的情况下(7500条左右),全选完成完全没有卡顿,多次测试完成时间都控制在100ms之内,贴一下performance中的截图
性能好的原因分析
比如说我们传入的数据结构如下:
data2:[{
name: '江苏',
id: '01',
expand: true,
children: [{
name: '南京',
id: '0101',
}, {
name: '苏州',
id: '0102',
children: [{
name: '吴江',
id: '010201'
}, {
name: '常熟',
id: '010202'
}]
}]
}, {
name: '云南',
id: '02',
children:[...]
}, {
name: '福建',
id: '03',
children:[...]
}],
我们选中苏州的时候会首先去遍历它的自己子节点,吴江和常熟,给自己子节点都加上选中的标记,然后直接去更新父节点的选中状态。更新完成以后就可以重新渲染组件。我们假设有n条数据,树形结构有5层,最坏的结果就是全选根节点,这样每一条数据都都需要去遍历一般更新一下选中状态,时间复杂度是 O(n), 可以说计算量是很小的。我们也不会说从根节点出发,去寻找到每一个节点,然后去触发它的点击事件,而是直接在根组件里面更新数据,然后更新页面,没有其他多余操作,自然全选也就比较流畅了。测试了一下几万条数据量,依旧是十分流畅,没人任何卡顿。