高性能tree组件(一种高性能的Tree组件实现方案)(1)

作者: 魁武 淘系前端团队

转发链接:https://mp.weixin.qq.com/s/4AIuLKhtOvgqxB15esFPqA

作为阿里经济体前端委员会四大技术方向之一,IDE 共建项目也进入第一阶段最后冲刺。在研发过程中经历许多困难与思考,项目也从最初的踌躇不决到如今的清晰笃定。

本篇 《IDE 共建深入浅出》 系列分享,由阿里经济体前端委员会 IDE 共建项目组策划,与大家分享 IDE 共建项目中技术与思考的点点滴滴。


NO.1 背景

2019年初,有幸参与了集团IDE 共建项目组, 打造阿里生态体系内的公共IDE底层。

一款IDE中,Tree 组件可能是所有视图中出现概率最高的一种视图形态,许多功能的基本交互形态也是落在 Tree 组件之中,其中不乏使用频率较高的文件树、调试变量树以及其他视图中的各式各样的树组件,可以这么说,Tree 组件的性能好坏会直接影响整个 IDE 的使用体验,在共建项目中,先后经历了两次的 Tree 组件实现,本文将通过介绍最近的一次重构,为剖析当前 KAITIAN框架 中的一种高性能Tree组件的实现。

NO.2 Tree组件长啥样?

常见的Tree组件如下图所示,主体结构一般是由普通的节点及可折叠的节点组成,除开主体,其他诸如一些选中状态,描述信息等,这些不同的装饰(渲染)方式构成了Tree组件的多样性。

高性能tree组件(一种高性能的Tree组件实现方案)(2)

以下介绍均以文件树为例

由此我们可以推导出,我们最终实现的Tree组件,应该为一个最基础的Tree形态,上层通过不同的拼装方式组合出真正使用的组件形态:

高性能tree组件(一种高性能的Tree组件实现方案)(3)

NO.3 预期的数据结构

由上面的Tree结构,我们可以简单的推导出我们的基础数据结构如下(左侧),而通过对基础类型的数据进行拓展,我们便可以得到更多类型的Tree节点定义,以文件树为例:

高性能tree组件(一种高性能的Tree组件实现方案)(4)

这样我们便构造出了我们需要的文件树组件中的节点定义,而通过这种组合,我们便可以通过上层复用底层Tree组件实现的能力,但也不会受限于底层Tree的实现,我们可以通过上层定义每个节点的实际渲染方式。

NO.4 定义事件驱动模型

由上面的使用方式,我们自然得能够想到我们需要定义好父的组件的通信方式,来保证父组件可以随时了解子组件状态并介入某些节点操作。如图所示:

高性能tree组件(一种高性能的Tree组件实现方案)(5)

基础的事件发送/订阅模型如下:

exportinterface ITreeWatcher { // 监听watcheEvent事件,如节点移动,新建,删除 onWatchEvent(path: string, callback: IWatcherCallback): IWatchTerminator; // 监听所有事件 on(event: TreeNodeEvent, callback: any); // 触发父节点即将变化事件 notifyWillChangeParent(target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode); // 触发父节点变化事件 notifyDidChangeParent(target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode); // 触发节点即将销毁事件 notifyWillDispose(target: ITreeNodeOrCompositeTreeNode); // 触发节点销毁事件 notifyDidDispose(target: ITreeNodeOrCompositeTreeNode); // 触发节点即将接收移动/新建/删除等事件(主要在文件树中使用) notifyWillProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent); // 触发节点接收移动/新建/删除等事件(主要在文件树中使用) notifyDidProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent); // 触发节点折叠状态即将变化事件 notifyWillChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean); // 触发节点折叠状态即将变化事件 notifyDidChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean); // 触发节点子节点即将变化事件 notifyWillResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean); // 触发节点子节点变化事件 notifyDidResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean); // 触发节点路径变化事件 notifyDidChangePath(target: ITreeNodeOrCompositeTreeNode); // 触发节点补充信息变化事件 notifyDidChangeMetadata(target: ITreeNodeOrCompositeTreeNode, change: IMetadataChange); // 触发节点分支变化事件 notifyDidUpdateBranch(); // 销毁函数 dispose: IDisposable; }

所有节点中都包含监听模型引用,通过在各个子节点粒度中埋上事件触发点,这样在根节点中我们便能通过一个 on 函数监听到整棵树的变化,从而去实现诸如快照、操作回滚等高级的树组件应用。

NO.5 定义节点渲染模型

一个基础的Tree组件我们可以看成一个长列表,基础的节点渲染样式如下:

高性能tree组件(一种高性能的Tree组件实现方案)(6)

通过控制不同层级关系下的缩进样式,便可以变成“更像”一个Tree组件

小知识点:页面中只要有使用了css3中的 transform: translateY(0) 属性便可开启chrome浏览器中的硬件加速能力,能获得更加良好的运行性能。

高性能tree组件(一种高性能的Tree组件实现方案)(7)

对于一个可能有上千个节点的长列表,为了性能上的考虑,我们往往只会选择性的选取部分节点渲染到页面上,为了达到更好的滚动效果,我们需要在前后预渲染部分节点(留给浏览器渲染一些反应时间),如下所示:

高性能tree组件(一种高性能的Tree组件实现方案)(8)

在滚动到边界的时候更新当前列表的初始渲染下表,结合 React的Diff 更新算法,从而实现节点回收的效果。

NO.6 如何获取列表数据

每个可折叠节点都需要具备获取子节点数据的能力,故需要为每个节点定义一个 TreeService 用于获取数据等能力,如图所示:

高性能tree组件(一种高性能的Tree组件实现方案)(9)

文件树通过在定义 File 及 Directory 的过程中便可定义其独有的数据获取方式。

整颗文件树渲染的主要数据来源为根节点下的 FlattenedBranch 二维数组中,该数据是将所有子节点信息平铺后的计算结果,展开根节点后基础数据结构示例如下:

注: 下标 代表了节点在渲染时的实际位置,如下表 0 代表渲染 Tree 组件是第一个节点; 节点ID 则代表了从数据源中获取节点数据实体的唯一标识,可以理解为有了ID后便能获取到节点所有渲染的必要信息。

高性能tree组件(一种高性能的Tree组件实现方案)(10)

下标代表则Tree组件在列表中的渲染位置,而节点ID着对应着一个具有TreeNode数据结构的节点

高性能tree组件(一种高性能的Tree组件实现方案)(11)

假定这里的节点101为一个可折叠节点,展开后根节点数据也会随之更新

高性能tree组件(一种高性能的Tree组件实现方案)(12)

折叠节点101后,对应的子节点将会重新存储与101节点的 FlattenedBranch 中,用于下次展开时使用(达到缓存的效果):

取出节点101对应的节点数据:

高性能tree组件(一种高性能的Tree组件实现方案)(13)

存储数据到节点101

高性能tree组件(一种高性能的Tree组件实现方案)(14)

通过这样便可在一个计算复杂度为N的数据结构上去操作我们列表的渲染数据。

NO.7 状态模型

讲完渲染,下一步便是对Tree组件状态的记录工作,我们需要定义一个状态模型,用于记录当前Tree组件的状态,核心数据状态如下:

高性能tree组件(一种高性能的Tree组件实现方案)(15)

通过在根节点绑定一个状态模型,我们既可以随时随地获取当前Tree的展示状态

NO.8 装饰器模型

到这一步,实际上我们的Tree组件已经具备了初步的能力,可以展示,也可以折叠,但我们依旧需要一种节点装饰手段来提供给开发者美化(装饰)节点的能力,装饰器模型便是来为我们提供这种装饰能力的重要手段。

高性能tree组件(一种高性能的Tree组件实现方案)(16)

NO.9 最终形态

综上,我们可以得到我们Tree组件的最终形态如下:

高性能tree组件(一种高性能的Tree组件实现方案)(17)

编码结构

我们最终实现文件树需要组装的大体结构如下:

1.文件树节点定义:

exportclass Directory extends CompositeTreeNode { .constructor( tree: ITree, publicreadonly parent: CompositeTreeNode | undefined, public uri: URI = new URI(''), public name: string = '', public filestat: FileStat = { children: [], isDirectory: false, uri: '', lastModification: 0 }, public tooltip: string, ) { super(tree, parent); if (!parent) { // 根节点默认展开节点 this.setExpanded(); } } } exportclass File extends TreeNode { constructor( tree: ITree, public readonly parent: CompositeTreeNode | undefined, public uri: URI = new URI(''), public name: string = '', public filestat: FileStat = { children: [], isDirectory: false, uri: '', lastModification: 0 }, public tooltip: string, ) { super(tree, parent); } }

2.Tree(Service) 定义:

定义Tree组件的数据获取来源 resolveChildren ,以及定义节点排序逻辑 sortComparator

exportclass FileTreeService extends Tree { async resolveChildren(parent?: Directory){ ... 获取子节点逻辑 } sortComparator(a: ITreeNodeOrCompositeTreeNode, b: ITreeNodeOrCompositeTreeNode) { ... 节点排序逻辑 } }

3.TreeModel 定义:

主要用于定义Tree渲染的数据模型,这里的数据与视图渲染可以是分离的两个过程,视图可选择时机从数据中获取对应位置的展示内容,而数据操作则是脱离于视图的,纯粹的数据操作,这在做一些数据预装/预处理等能力的时候会十分好用。

exportclass FileTreeModel extends TreeModel { constructor(@Optional() root: Directory) { super(); // 还可以在这里做一些其他初始化工作 } }

4.节点渲染模型定义:

每个视图可以自定义自己的节点渲染模型,如下面的 FileTreeNode,基于这种灵活的组装形式,可让Tree组件具备更灵活的拓展性。

const renderFileTree = () => { if (isReady) { return<RecycleTree height={filterMode ? height - FILTER_AREA_HEIGHT : height} width={width} itemHeight={FILE_TREE_NODE_HEIGHT} onReady={handleTreeReady} model={fileTreeModelService.treeModel} filter={filter} > {(props: INodeRendererProps) => <FileTreeNode item={props.item} itemType={props.itemType} decorationService={decorationService} labelService={labelService} dndService={fileTreeModelService.dndService} decorations={fileTreeModelService.decorations.getDecorations(props.item)} onClick={handleItemClicked} onTwistierClick={handleTwistierClick} onContextMenu={handlerContextMenu} />} </RecycleTree>; } };

至此便可定义一个完整的文件树组件渲染及数据结构

10 性能对比
  1. 新文件树 VS 旧文件树 性能对比

高性能tree组件(一种高性能的Tree组件实现方案)(18)

  1. 新文件树 VS VSCode文件树 性能对比

高性能tree组件(一种高性能的Tree组件实现方案)(19)

在经历二次Tree组件重构后,整体文件树性能得到了较大程度的性能优化,甚至在当前场景下得到了比VSCode文件树更加优异的性能表现。

综合对比VSCode中的实现,目前带来明显性能差异点在于:

  1. 对比VSCode为直接的Dom操作,React的DOM Diff算法在大文件场景下有一定性能优势
  2. 扁平化数据模型,去递归化
  3. 在父组件中独立定义渲染模型,所有渲染均是按需分配,而不必在基础Tree组件中进行冗余判断
11 性能之外

除了性能上的提升外,Tree组件的设计还为我们提供了更多可能性:

  1. Tree组件的设计更重要的是为组件使用者提供了构造一个类似组件的参考,通过固定的组装模式便可产出对应的SearchTree、GitTree、TodoTree 等,一切仅需三步,定义Model、定义节点数据、获取数据后渲染节点即可,更提供了如刷新、定位、折叠全部等快捷功能调用,用简单的数据结构描述来让开发者从定义Tree的“地狱”中解放出来。
  2. 数据与渲染模型分离的设计能让我们在页面未加载前便准备好渲染数据,一切的数据操作可静默完成,在渲染时则立即可用于渲染,这在优化二、三屏加载速度场景中将会十分好用。
  3. 结合DI,我们还可以实现继续数据模型的多例实现,从而达到通过一个DI Token,产生一颗树的能力。

12 总结

当前 KAITIAN框架 中的Tree组件改造仅经历了第一阶段的文件树改造,相关代码仍处于不断优化改进过程中,本文仅为大家分享一种可行的实现思路,后续我们将会在性能及功能稳定后将框架内的所有类Tree视图进行替换,并孵化出更易用的Tree组件用于其他场景下的建设。

推荐JavaScript经典实例学习资料文章

《进击的JAMStack》

《前后端全部用 JS 开发是什么体验(Hybrid Egg.js经验分享)上》

《前后端全部用 JS 开发是什么体验(Hybrid Egg.js经验分享)中》

《前后端全部用 JS 开发是什么体验(Hybrid Egg.js经验分享)下》

《一文带你搞懂 babel-plugin-import 插件(上)「源码解析」》

《一文带你搞懂 babel-plugin-import 插件(下)「源码解析」》

《JavaScript常用API合集汇总「值得收藏」》

《推荐10个常用的图片处理小帮手(上)「值得收藏」》

《推荐10个常用的图片处理小帮手(下)「值得收藏」》

《JavaScript 中ES6代理的实际用例》

《12 个实用的前端开发技巧总结》

《一文带你搞懂搭建企业级的 npm 私有仓库》

《教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?》

《细说前端开发UI公共组件的新认识「实践」》

《细说DOM API中append和appendChild的三个不同点》

《细品淘系大佬讲前端新人如何上王者「干货」》

《一文带你彻底解决背景跟随弹窗滚动问题「干货」》

《推荐常用的5款代码比较工具「值得收藏」》

《Node.js实现将文字与图片合成技巧》

《爱奇艺云剪辑Web端的技术实现》

《我再也不敢说我会写前端 Button组件「实践」》

《NodeX Component - 滴滴集团 Node.js 生态组件体系「实践」》

《Node Buffers 完整指南》

《推荐18个webpack精美插件「干货」》

《前端开发需要了解常用7种JavaScript设计模式》

《浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务》

《了不起的 Webpack HMR 学习指南(上)「含源码讲解」》

《了不起的 Webpack HMR 学习指南(下)「含源码讲解」》

《10个打开了我新世界大门的 WebAPI(上)「实践」》

《10个打开了我新世界大门的 WebAPI(中)「实践」》

《10个打开了我新世界大门的 WebAPI(下)「实践」》

《「图文」ESLint 在中大型团队的应用实践》

《Deno是代码的浏览器,你认同吗?》

《前端存储除了 localStorage 还有啥?》

《Javascript 多线程编程​的前世今生》

《微前端方案 qiankun(实践及总结)》

《「图文」V8 垃圾回收原来这么简单?》

《Webpack 5模块联邦引发微前端的革命?》

《基于 Web 端的人脸识别身份验证「实践」》

《「前端进阶」高性能渲染十万条数据(时间分片)》

《「前端进阶」高性能渲染十万条数据(虚拟列表)》

《图解 Promise 实现原理(一):基础实现》

《图解 Promise 实现原理(二):Promise 链式调用》

《图解 Promise 实现原理(三):Promise 原型方法实现》

《图解 Promise 实现原理(四):Promise 静态方法实现》

《实践教你从零构建前端 Lint 工作流「干货」》

《高性能多级多选级联组件开发「JS篇」》

《深入浅出讲解Node.js CLI 工具最佳实战》

《延迟加载图像以提高Web网站性能的五种方法「实践」》

《比较 JavaScript 对象的四种方式「实践」》

《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》

《前端如何一次性处理10万条数据「进阶篇」》

《推荐三款正则可视化工具「JS篇」》

《如何让用户选择是否离开当前页面?「JS篇」》

《JavaScript开发人员更喜欢Deno的五大原因》

《仅用18行JavaScript实现一个倒数计时器》

《图文细说JavaScript 的运行机制》

《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》

《推荐Web程序员常用的15个源代码编辑器》

《10个实用的JS技巧「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你内存泄漏如何防范》

《手把手教你7个有趣的JavaScript 项目-上「附源码」》

《手把手教你7个有趣的JavaScript 项目-下「附源码」》

《JavaScript 使用 mediaDevices API 访问摄像头自拍》

《手把手教你前端代码如何做错误上报「JS篇」》

《一文让你彻底搞懂移动前端和Web 前端区别在哪里》

《63个JavaScript 正则大礼包「值得收藏」》

《提高你的 JavaScript 技能10 个问答题》

《JavaScript图表库的5个首选》

《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》

《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》

《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》

《可视化的 js:动态图演示 Promises & Async/Await 的过程》

《原生JS封装拖动验证滑块你会吗?「实践」》

《如何实现高性能的在线 PDF 预览》

《细说使用字体库加密数据-仿58同城》

《Node.js要完了吗?》

《Pug 3.0.0正式发布,不再支持 Node.js 6/8》

《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》

《JavaScript 20 年 中文版之创立标准》

《值得收藏的前端常用60余种工具方法「JS篇」》

《箭头函数和常规函数之间的 5 个区别》

《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传下载那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成下载》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传下载那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

作者: 魁武 淘系前端团队

转发链接:https://mp.weixin.qq.com/s/4AIuLKhtOvgqxB15esFPqA

,