阅读 2247

横向对比Element-tree、ztree、ivew-tree性能对比分析与源码修改自定义组件

一、前言

公司的树业务复杂的一批,而且数据量十分大,动不动测试就说最少测上w条数据,我顶个狒狒。

公司的中后台系统用的是Element-UI框架,自然树组件也就是用的el-tree。

说实话,el-tree已经很不错了,提供丰富的回调事件,属性,方法,在轻量数据渲染上可以说是不二之选。

但是奈何半年多的项目实践,越发觉得el-tree卡顿了起来,数据越来越多,功能越来越复杂。

测试提这方面的BUG已经到了测必提的境界,优化树组件性能势在必行!!!

寻找一条通往光明的道路。

二、可以胜任上W条数据渲染的Tree

2.1 vue-giant-tree

于是擅长找资源的我,找啊找,咦,找到一个很霸气的家伙:

你看它牛的,轻松实现海量数据的高性能渲染。

关键词:轻松、海量、高性能

还解释了为什么要用这玩意:

来来来,有不服气吗? ===》 big-tree.json

彭大师表示不服,安排!

首先把这个号称big-tree的json下载下来看看是真是假!!

大小

内容

一顿git clone操作,先是yarn,然后serve跑起来,F12按一按。

肉眼看:加载的非常快!眨眼之间!

看下dom加载:

???

确实牛逼啊!!!

但是这个vue-giant-tree就像它自己说的那样,仅仅是套了一层ztree的壳,修改下皮肤,所有的功能都是ztree的,所以我去ztree官网仔细看了一下文档。

仔细研究后,发现ztree确实牛逼!!可以在IE6运行且性能十分棒,但是它的icon图标十分的简陋,而且采用是的雪碧图方式,用绝对定位去获取每个图标,这点不符合我们公司的业务需求,咱们要的是好看的,漂亮的,可随时更换的图标。

公司的UI小姐姐,说提供一份位置一样大小一样的雪碧图有难度,不一定能搞出来。

于是我打消了这个念头,决定修改ztree源码采用iconfont形式去获得灵活且丰富的icon配置。

2.2 来来来,站在巨人的肩头上

牛顿曾言:我之所以这么成功,是因为我站在巨人的肩膀上!

我彭大师如今脚踩vue-giant-tree和ztree,产出了可以胜任业务复杂又要海量数据渲染的vue-magic-tree!

当当当!!!

亮眼表现:

  • 1.卖相十佳,采用vue-giant-tree的漂亮皮肤。
  • 2.提供iconfont图标库,任意渲染任意节点类型的图标。

随机渲染多种icon图表

这样,这颗参天大树就满足了业务复杂程度为O(n * n)的业务了。

介绍以下业务:

  • 1.默认展开某个分支的某个节点下的所有节点。(一句话说,我想要这个展开,那个不展开,这个展开到多少层,那个又多少层,项目经理吃屎)
  • 2.每种节点类型都要有相应的icon图标,有的节点不需要。
  • 3.渲染上w条数据不能有“正在刷新的状态”(项目经理吃屎)
  • 4.还有一系列要求,我就不展示了。

光上上面3点,el-tree退出游戏,i-tree退出游戏。

有的童鞋可能会说,el-tree和i-tree都有提供自定义渲染功能,也能实现icon图标自定义啊。这样说的同学你可以试试,数据量多的时候,有多卡。而且通篇文章只比较初始化加载时间,根本没有比较勾选事件。我们公司的el-tree在3000条数据然后点击全选(点击根节点的checkbox)就要卡2-3秒,每次取消、勾选、取消、勾选,卡的一匹。

不相信的话,我只能说你自己体会。

接下来看性能对比测试,不想看的同学可以点击直达源码修改章节

三、摆个擂台挑战下(初始化渲染树)

规则说明:

1.同样一份文件,本地加载(ivew的节点名称必须为title, 所以iview那份数据中的name都改成了title, 看我github仓库代码可以看到iview加载的不是全局这份数据,但是是一样的。)

const bigData = require("@/mock/big-tree.json");
复制代码

2.同样适用清空缓存重新刷新。

3.gif同样的位置,大小,比率。

保证了一致性,可以明显看出,iview和element加载明显有卡顿,而ztree几乎没有延迟。

线上测试地址:测试 (海外服务器加载可能比较慢)

3.1 我尊贵Vip iview表示不服!

3.2 我平头哥element表示不服!

3.3 Ztree 牛逼

3.4 结果分析

牛批!!

我要说下这份数据是一个一维的数组,ztree根据pid自动拼接成一颗树形结构的数据然后渲染,还这么出色!!!

而且ztree的功能异常丰富,可以满足你一切需要,你要自己看文档,结合你的业务去开发。

我在这里只是想说ztree牛逼!!!

四、对ZTree动手动脚

嘿嘿,油头蓬面大叔来了

十八禁哈

既然知道这颗树如此优秀核心还是在于ztree,那么咱们就把它的开源代码下载下来,搞起来!!!

这就是ztree的源码(JQuery zTree v3.5.41)

可以看到目录结构很简单,实现ztree的都在这个js文件夹中。(所以只聚焦关注这个文件夹)

4.1 从命名分析

一共十一个文件,(按顺序)第一个是jquery的min版(不管不看)。然后粗略看了眼,带min的文件都给它忽略掉!

这样就剩下5个文件:

jquery.ztree.all.js
jquery.ztree.core.js
jquery.ztree.excheck.js
jquery.ztree.exedit.js
jquery.ztree.exhide.js
复制代码

从命名上看,很清晰了其实:

  • all应该是所有功能的集合。
  • core就是核心功能。
  • ex开头的都是扩展功能。

因为引入项目的是all文件,其实只看all就行了哈。(all包含了core和ex的所有)

动手!!!

因为咱们需要改变它陈旧古老的icon节点,所以从文档入手

我们可以找到控制节点图标的功能设置是通过一个叫做setting.view.showIcon变量控制的

setting.view.showIcon: 控制节点图标显示与否。

那么咱们就看看这玩意在源码中是怎么处理逻辑的,从而找到突破口。

4.2 从全局看

打开all文件:其实里面备注什么都很详细和完善。

定义了:

  • _consts常量
  • _setting默认配置对象
  • 一些初始化方法_init*,
  • 事件绑定和解绑(_bingEvent、_unbindEvent),事件代理(_eventProxy)。

继续往下看:

定义了:

  • 操作数据的方法集合(data)
  • 事件代理的方法集合(event)
  • 事件处理的方法集合(handler)
  • 为ztree提供公共方法的结合(tools)
  • 渲染DOM的方法集合(view)
  • ztree的定义

其实很明了,我们想要修改icon图标所在的那个dom,所以就看view底下的方法即可;亦可用搜索showIcon关键字来找到对应方法的位置。不过先以大局观看整个项目比较好,这样脑子里有一个程序设计的完整的流程和生命周期这样的概念。

4.3 查看view底下的方法:

共计30-40个方法(粗略数了一下)

首先不要慌,先看函数名,相信自己能够看懂。

圈起来这个makeDOMNodeIcon,就是渲染icon和节点名称的那个方法。

4.4 makeDOMNodeIcon 渲染出节点icon和名称

makeDOMNodeIcon: function (html, setting, node) {
    var nameStr = data.nodeName(setting, node),
      name = setting.view.nameIsHTML ? nameStr : nameStr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    html.push("<span id='", node.tId, consts.id.ICON,
      "' title='' treeNode", consts.id.ICON, " class='", view.makeNodeIcoClass(setting, node),
      "' style='", view.makeNodeIcoStyle(setting, node), "'></span><span id='", node.tId, consts.id.SPAN,
      "' class='", consts.className.NAME,
      "'>", name, "</span>");
  },
复制代码

很简单,这个方法就做了一件事,往html里面push了两个span标签,一个是用于icon图标的,一个用于显示节点名称的。

我们需要的修改icon图标这个dom,仔细查看一下,icon这个dom的class和style分别通过makeNodeIcoClassmakeNodeIcoStyle去动态的获取。

这里我要介绍下,iconfont图标渲染形式——font-class引用:

<i class="iconfont icon-xxx"></i>
复制代码

所以我们只需要给这个节点的icon图标dom加上这两个类即可渲染出iconfont形式的图标。

往下翻看:

4.5 makeNodeIcoClass 给节点icon写上类名

先看makeNodeIcoClass

makeNodeIcoClass: function (setting, node) {
    var icoCss = ["ico"];
    if (!node.isAjaxing) {
      var isParent = data.nodeIsParent(setting, node);
      icoCss[0] = (node.iconSkin ? node.iconSkin + "_" : "") + icoCss[0];
      if (isParent) {
        icoCss.push(node.open ? consts.folder.OPEN : consts.folder.CLOSE);
      } else {
        icoCss.push(consts.folder.DOCU);
      }
    }
    return consts.className.BUTTON + " " + icoCss.join('_');
  },
复制代码

这个方法就像名字那样,很直白,就是return一个字符串,即一些类名。

consts.className.BUTTON这个是常量,可以在_consts常量列表中找到值就是:

然后icoCss就是固定一个ico + open close docu 三选一拼接而成,如果设置了iconSkin属性那么就拼上这个。 也就是得到了两个类名吧:button ico_open这样形式的。

其实这些不重要,因为我们将完全彻底的改写这个方法。

改写如下:

 makeNodeIcoClass: function (setting, node) {
    const iconName = data.nodeIcon(setting, node);
    return `${consts.className.BUTTON} iconfont ${iconName}`; 
 }
复制代码

咱们通过data集合中的一个nodeIcon方法得到iconName,然后拼接成button iconfont icon-xxx形式。

保留consts.className.BUTTON是因为有样式,我懒得改。这样就可以渲染出iconfont的图标了。

看下data.nodeIcon(此方法放入data集合中):

nodeIcon: function (setting, node, newName) {
    const key = setting.data.key.nodeType;
    if (typeof newName !== 'undefined') {
      node[key] = newName;
    }
    return setting.data.iconMap[node[key]] || "";
  },
复制代码

首先获得全局设置对象中的setting.data.key.nodeType这个属性,然后得到node[key]=node.nodeType=0,然后通过key:value形式返回iconMap的对应的那个值,最终得到此节点的icon图标类名。

节点例如:

  { id: 1, pid: 0, name: "随意勾选 1", open: true, nodeType: 0, chkDisabled: true },
复制代码

setting对象中的属性:

在这个例子中,获得的值就是setting.data.iconMap[0] = 'iconjianyuede'

最后还做了一个判断,如果·node[key]iconMap中没有对应的key,也就是setting.data.iconMap[node[key]] = undefined,也有相应的返回值空串,这样做的好处是,有些节点不需要图标,那么在iconMap中就不要有相应的key,ztree就不会渲染图标。

4.6 makeNodeIcoStyle 给节点icon写上样式

控制图标显示与否showIcon相关的方法makeNodeIcoStyle

 makeNodeIcoStyle: function (setting, node) {
    var icoStyle = [];
    if (!node.isAjaxing) {
      var isParent = data.nodeIsParent(setting, node);
      var icon = (isParent && node.iconOpen && node.iconClose) ? (node.open ? node.iconOpen : node.iconClose) : node[setting.data.key.icon];
      if (icon) icoStyle.push("background:url(", icon, ") 0 0 no-repeat;");
      if (setting.view.showIcon == false || !tools.apply(setting.view.showIcon, [setting.treeId, node], true)) {
        icoStyle.push("width:0px;height:0px;");
      }
    }
    return icoStyle.join('');
  },
复制代码

里面就是通过isParentshowIcon一起控制icon图标dom的样式,如果显示就是给background设置路径,不显示就让它宽高为0,最终返回一个包含样式的字符串。

我们的业务不需要判断父节点图标显示与否,所以不用管isParent属性,只要配合showIcon即可,改写如下:

makeNodeIcoStyle: function (setting, node) {
    if(!node.isAjaxing) {
      if (setting.view.showIcon == false
          || !tools.apply(setting.view.showIcon, [setting.treeId, node], true)
          || data.nodeIcon(setting, node) === ''
        ) {
        const iconStyle = "display:none;"
        return iconStyle
      }else {
        return ''
      }
    }
  },
复制代码

如果显示返回空串,不显示返回display:none;即可。

4.7 来看看效果

首先项目中引入iconfont:

// main.js
import './style/iconfont.css'
复制代码

然后引用咱们魔改后的ztree源码文件all:

将它require进来,别忘了引入jquery!!!

// 引入jquery
import * as $ from "jquery";
if(!window.jQuery){
  window.jQuery = $;
}
// 引入魔改版ztree
require("./lib/jquery.ztree.all");

// 调用ztree官方初始化方法,参数省略
$.fn.zTree.init()
复制代码

最后效果就是这样:

效果不错,但是在你操作的时候,比如折叠这个节点会出现,此节点已有的图标会消失的问题。 所以,还需要继续看下源码接着修改。

4.8 折叠事件

在view集合中继续查看函数名,可以找到:

这三个方法,前缀都是expandCollapse意思就是展开节点这一类的意思。

展开expandCollapseNode方法,往下翻到1235行:

这里用一个node.open属性去判断调用replaceIcoClass方法,替换了icon图标dom的class。所以下一步,修改这个方法。

replaceIcoClass:

replaceIcoClass: function (node, obj, newName) {
    if (!obj || node.isAjaxing) return;
    var tmpName = obj.attr("class");
    if (tmpName == undefined) return;
    var tmpList = tmpName.split("_");
    switch (newName) {
      case consts.folder.OPEN:
      case consts.folder.CLOSE:
      case consts.folder.DOCU:
        tmpList[tmpList.length - 1] = newName;
        break;
    }
    obj.attr("class", tmpList.join("_"));
  },
复制代码

就是做了替换class的操作,不分析了,直接修改:

replaceIcoClass: function (node, obj, newName) {
    // 根据你的情况,我这边就全部删除就可以了
 },
复制代码

改完之后,折叠的时候节点图标不会消失,正常了。这样改完所有增删改功能都不会影响图标正常的显示了。

4.9 暂且完成一阶段任务

按照上面修改后,正常使用,可以满足业务的需求而且渲染海量数据上性能也十分出色。不过也只能说暂且完成一个阶段而已,毕竟狗需求天天改。。。

五、写在后面

我个人认为能看懂一些源码,且修改一些内容,并不厉害。毕竟会玩手机跟生产手机是一回事吗?

大家也不要妄自菲薄,其实很多源码并没有你想象的那些难,只是你认为自己不行,你的潜意识一直告诉自己:你看不懂。所以导致了大家误以为这些源码很难看懂。

其实我想告诉大家的是,多看源码,好处非常明显:

  • 1.看看大厂或者大神人家的代码是如何编写的,人家的代码规范是怎么样的?
  • 2.可以学到更多高阶函数的用法,大家面试的时候不是经常背一些什么函数柯里化,闭包,设计模式,compose等等,但是基本在自己的接触项目中不会用到,而这些好的开源项目中的源码内你就可以发现一些影子,你可以试着理解试着运用起来。
  • 3.可以了解整个程序设计需要考虑的东西,这些开源项目的完整性和可维护性都非常的强,可以从中学习,并且提升自己。

GitHub链接:vue-magic-tree 希望能过帮到大家。 线上测试地址:测试

关注下面的标签,发现更多相似文章
评论