![acc1b790fa41493e8306942f9abfa73b.png](https://i-blog.csdnimg.cn/blog_migrate/51577612995fdd907325d45b44177bfe.png)
在一个晴空万里、可以哼着小曲上着班的一天,突然一声晴天霹雳!
远处传来一声来自测试的声音:“来人啊!我的浏览器‘喔唷,崩溃了’!”。我只能连跑带冲的走过去,看了一眼之后熟练地结束掉标签页,并询问他发生了什么。
“我没做什么,我就是在这个页面创建了 1500 条数据~”
![5374f2a19ca091dfeabdc96b8639c500.png](https://i-blog.csdnimg.cn/blog_migrate/500f3edd67345541a0903d825f794bc7.png)
测试工程师果然是软件的护城河!一个需求计划最多只会出现 40 条数据的功能,测试准备了 37.5 倍的数据测试,我瞬间又对他们肃然起敬!但,问题总是要解决的
TL;DR
- 如果你也有同样需求且用不上懒加载或者拖拽功能,可以试试使用经历过定制和更新的 fork 版本:FEMessage/element
- 大致分析问题:为什么会导致卡得不行
- 本次更新目的:最小成本实现虚拟列表
分析诱因
先整个双层 500 节点(1x500)的数据看看,通过 chrome 的性能工具或许可以发现到什么
![67ce9f0759540126b4cc8361c10cdc1f.png](https://i-blog.csdnimg.cn/blog_migrate/22881ae70dd328ac2a998dfd1198f56b.png)
接着,启动性能录制,分别做打开节点、全选节点、全反选节点、收起节点,中间间隔 2s。得到这个结果
![2853167b028d8e2820c482e77ee07b30.png](https://i-blog.csdnimg.cn/blog_migrate/0d7cfd4327a6b46d351bf8371dddd0e4.png)
用过这个玩意的看颜色都知道发生什么了。其中:黄色是脚本耗时,紫色是渲染耗时。截图没有更多信息,不过这里可以说明一下,大部分脚本运算时间都被一个叫 createChildren 的方法所侵蚀,这个方法是 vue diff 工作部分的用来创建节点的函数
这意思就是展开的瞬间对比突然出现大量差异,造成不断创建?直接去看源码,发现在 tree-node.vue#L60 出现了递归组件
![9bd47f83ba6903a4b7f98f631af53cd7.png](https://i-blog.csdnimg.cn/blog_migrate/3fca229b95572ad4c896f3a895abfb37.png)
这里跟 tree.vue#L12 的使用方式是直接一致的,列表创建组件
![cff25cc060543d8e5410bdd34cc87454.png](https://i-blog.csdnimg.cn/blog_migrate/392fec83c73db8712a613f7435517938.png)
这个组件跟经常看的经常写的组件不太一样 —— 核心逻辑不在 vue 文件,它把状态管理都转移到了外部。由两个类来管理整个状态(state)树,所以核心的逻辑在 node.js
先看 tree-node.js#L17 可以发现,组件的做法是直接用顶点创建一个节点,接下来属于该枝干的节点就由这个枝干完成递归工作了。那节点一多不就算起来了?
(吐槽一下这组件的命名,models 叫 node, tree-store。组件又叫 tree, tree-node,第一观感的文件名是否存在 '-',在实际上的对应竟然是反过来的! )
最小实现
在这之前先看了一下 issue 区,emm
![a10255f3d22b29527054f034c616ab0a.png](https://i-blog.csdnimg.cn/blog_migrate/88521bec04d991154d1e951c07807d25.png)
![5f4a45e73742f5ad5070f8d69129145d.png](https://i-blog.csdnimg.cn/blog_migrate/4708daf7a72bc6d2ae4e21599e249782.png)
![0bdde9a3ca28418d76200c0096c71dd7.png](https://i-blog.csdnimg.cn/blog_migrate/165cd9d3ed46208e0ee71d1e464eee0b.png)
巧了我们也遇到了!但是我们决(bi)定(xu)解决这个问题!于是想到了一个所有人都能做到的解决方案,我们称为「最小成本」方案
所以这次要实现的「最小成本」,定义为以下:
- 因为状态管理独立且外置,可以继续利用其数据结构。也是「最小成本」核心思路
- 问题可以拆分为节点计算问题与渲染问题,先解决节点问题:递归改为列表
- 本质上是节点过多导致本身渲染能力低下,添加虚拟滚动解决
首先是列表。从上面可得:树形结构靠节点递归实现,最顶点节点被赋值为 root
。所以定义一个计算属性
visibleList() {
return this.flattenTree(this.root.childNodes);
}
而 flattenTree
的实现思路非常普通:数组合并,列表不需要管理层级,如图例所示:
![55d476e2a82c7b3c32f35ada549d41e5.png](https://i-blog.csdnimg.cn/blog_migrate/2492a86484a4e1552a79702f07fb08fc.png)
所以需要做的只是按顺序把节点塞进数组中。需要处理的是:当节点折叠被展开时,把节点的子节点添加到列表中,反之不需要
flattenTree(datas) {
return datas.reduce((conn, data) => {
conn.push(data);
if (data.expanded && data.childNodes.length) {
conn.push(...this.flattenTree(data.childNodes));
}
return conn;
}, []);
}
当然这么做也存在性能问题 —— 数组一产生变化就会全盘重新计算。只能说在这个需求下,问题不那么明显。
列表问题解决之后,接着是虚拟滚动问题
继续「最小成本」,虚拟列表直接使用组件 vue-virtual-scroll-list
vue-virtual-scroll-listgithub.com因为这个组件要求 list item 需要做一定定制(通过source
传递信息),意味着这对原有逻辑存在有损,需要先隔离逻辑
这样做的目的是:如果我不需要虚拟滚动的特性,我可以继续使用原本组件,并且不承担新改动带来的副作用
因为组件分离了状态管理,所以这一步特别好处理:node -> source
<!-- 虚拟滚动独立逻辑与独立组件 -->
<virtual-list v-if="height" :style="{ height: height + 'px', 'overflow-y': 'auto' }"
:data-key="getNodeKey"
:data-sources="visibleList"
:data-component="itemComponent"
:keeps="Math.ceil(height / 22) + extraLine"
:extra-props="{
renderAfterExpand,
showCheckbox,
renderContent,
onNodeExpand: handleNodeExpand
}"
/>
<el-tree-node
v-else
v-for="child in root.childNodes"
:node="child"
:props="props"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox"
:key="getNodeKey(child)"
:render-content="renderContent"
@node-expand="handleNodeExpand">
</el-tree-node>
接着,通过观察原本的组件发现,因为改变了节点之间的逻辑,所以节点拖拽会失效。由于这个版本不会修复这个问题,所以新的节点组件中就干脆把与拖拽相关的事件全部移除(精炼了组件代码…)
关于虚拟节点的具体代码可以查看 FEMessage/element:tree-virtual-node.vue
至此,修改结束
展示效果
在开发之前曾拜访过“世界第二流行的 React UI 框架”,该库的 tree 组件已经实现了虚拟滚动。这不皮一下怎么行?直接快进到「在 CodeSandbox 中打开」并删除 height
属性!然后就是喜闻乐见的标签页崩溃了…
于是喜出望外,决定使用一样的 demo 生成函数来进行测试!
测试结果录了一个 GIF,结果如下:
![40e42087c146c2e74538fc3edc011b29.png](https://i-blog.csdnimg.cn/blog_migrate/cecfc31978b5ab75cedd8878c9602cab.png)
如果不信这个结果,或者感兴趣而且对自己的计算机性能持有充足信心可以试试这个函数,看看在当前两个流行框架所代表组件库的 tree 的表现(不开启虚拟滚动的前提下)
当前问题
细心的朋友肯定会发现,这次改动有一个 transition 容器组件因无处安放而被移除掉了,再加上即使要做回动画,现在的动画逻辑也与 el-collapse-transition
所定义的不同,所以这个版本很遗憾,节点折叠没有动画
其二是懒加载失效,因为节点的生成不再是由枝干节点决定,所以失效了
其三是拖拽能力失效,因为节点之间的逻辑已经打破,加上「最小成本」计划是继续复用所有数据结构,所以没有实现
如果你的业务中需要动辄上千节点的长列表,又都不受这三个特性影响的话,很建议采用这种方式做一次优化。或者采用更方便的方式 —— 直接使用 FEMessage 修改过的 element,这个包除了类似这种对原本组件的修改和提升,还有一些业务提炼出来的业务组件可以使用
同时也挖了个坑(x2),计划是这样的:
- 恢复节点动画
- 恢复拖拽功能
可以不抱期望的期待一下,我们接下来对这个组件的改动与文章~
参考连接
- and design - tree:https://ant.design/components/tree-cn/
- vue-virtual-scroll-list:https://github.com/tangbc/vue-virtual-scroll-list