关于技术分享:Tree组件在海量数据时的性能优化虚拟树

47次阅读

共计 4422 个字符,预计需要花费 12 分钟才能阅读完成。

前些日子我给 Santd 的 Tree 组件减少了虚构树的性能,用于解决 Tree 组件在渲染海量数据时的性能问题。

虚构树这个货色,大家当前在前端业务中可能有机会用到,即便没有间接用到,理解一下虚构树兴许也能给大家提供一点解决问题的新思路。

Santd 是 Ant Design 的 San 实现,服务于企业级中后盾产品。

0 目录

1 什么是虚构树?

2 为什么要用虚构树?

3 怎么实现虚构树?

4 最终成果

5 参考资料

1 什么是虚构树?

可能有的读者还晓得一个概念,叫“虚构列表”。

虚构树和虚构列表实质其实是一样的,只不过前者的原始数据的构造是树,而后者的原始数据的构造是列表,以及最初的表现形式不一样。

不论是虚构树还是虚构列表,外围原理都是只渲染可视区域内的数据,也就是说,用户看不到的数据就不渲染了,这也是之所以称之为“虚构”的起因。

一言以蔽之,虚构树就是只渲染可视区域的数据的树。

2 为什么要用虚构树?

因为渲染耗时。

如果一棵树含有海量数据,比方上万条,那么,这棵树的渲染耗时会很长。

咱们能够通过一个理论的例子直观感触下。

上面的代码是应用 Santd 的一般树组件创立一个有 11110 个节点的树,代码的具体细节咱们不须要关注,咱们只须要晓得它创立了一个有一万多个节点的树并打印出了树的渲染工夫就能够了。


<template>
    <s-tree treeData="{{treeData}}" defaultExpandAll="{{true}}"></s-tree>
</template>

<script>
import {Tree} from 'santd';

function dig(path = '0', level = 3) {const list = [];
    for (let i = 0; i < 10; i += 1) {const key = `${path}-${i}`;
        const treeNode = {
            title: key,
            key,
        };

        if (level > 0) {treeNode.children = dig(key, level - 1);
        }

        list.push(treeNode);
    }
    return list;
}

const start = Date.now();
const treeData = dig();
setTimeout(() => {console.log(` 渲染耗时:${(Date.now() - start) / 1000} 秒 `);
}, 0);

export default {
    components: {'s-tree': Tree},
    initData() {
        return {treeData}
    }
}
</script>

而后,咱们就能够看到,这个 11110 个节点的树在我的 2019 款的 MacBook Pro 的 Chrome 里渲染了 26 秒,将近半分钟。


这么长的耗时个别是不能承受的,所以咱们须要想方法解决这个问题,这就能够用到虚构树了。

虚构树能够通过只渲染海量数据中的那局部在可视区域的数据来大幅缩短渲染耗时。

3 怎么实现虚构树?

实现虚构树的思路是,先把实现虚构树转化成实现虚构列表,而后在展现的时候把列表装璜得像一棵树。

依据这个思路,能够把实现虚构树的过程大抵分为 4 步:

  1. 把树结构的原始数据拍平;
  2. 计算哪些数据在可视区域;
  3. 模仿滚动;
  4. 把列表装璜成树。
    接下来咱们就一步一步来看。

临时不须要过多地关注代码细节,能够先整体把握虚构树的实现步骤和原理,而后之后有需要的话再回头来细看代码。

3.1 把树结构的原始数据拍平

咱们用简略的原始数据来阐明这一步。

原始数据会是像上面这样的树结构的:


const treeData = [
    {
        key: '0-0',
        children: [
            {key: '0-0-0'},
            {key: '0-0-1'}
        ]
    },
    {
        key: '0-1',
        children: [
            {key: '0-1-0'},
            {key: '0-1-1'}
        ]
    }
];

这是一棵简略的树,深度为 2,一级节点有 2 个,每个一级节点又各自有 2 个二级节点,所以一共是 6 个节点。

而后咱们要把它拍平,拍平的要害是按深度优先把树遍历一遍,拍平的逻辑如下:


function flattenTreeData(treeData) {const flatNodes = [];
    const dig = treeData =>
        treeData.forEach(treeNode => {flatNodes.push(treeNode);

            dig(treeNode.children || []);
        });

     dig(treeData);

    return flatNodes;
}

拍平之后的数据会像是上面这样的,这里须要重点关注下,因为之后咱们的所有解决都是基于这个拍平后失去的数组。

拍平后,全副的节点都成为了最外层的数组的项(原来只有一级节点是最外层的数组的项)。


const flatNodes = [
    {
        key: '0-0',
        children: [
            {key: '0-0-0'},
            {key: '0-0-1'}
        ]
    },
    {key: '0-0-0'},
    {key: '0-0-1'},
    {
        key: '0-1',
        children: [
            {key: '0-1-0'},
            {key: '0-1-1'}
        ]
    },
    {key: '0-1-0'},
    {key: '0-1-1'}
];

这时,如果咱们把这个数组遍历一遍并绘制在页面上(即列表渲染),失去的节点的程序和把原始数据渲染成树时的节点的程序是统一的,只不过前者没有缩进,看起来是个列表,而后者有缩进,看起来是个树,如上面两张图所示。

3.2 计算哪些数据在可视区域

实际上咱们须要解决的数据会有很多,而咱们只须要渲染进去其中的一部分,即在可视区域的那局部,所以咱们须要计算到底是哪些数据在可视区域。

可视区域的高度是固定的,假如树的每一个节点的高度也是固定的,那么,依据这两个数据,咱们就能够计算出可视区域内能展现多少个节点。


// VISIBLE_HEIGHT 可视区域的高度
// NODE_HEIGHT 树的节点的高度
// visibleCount 可视区域内能展现多少个节点
const visibleCount = Math.ceil(VISIBLE_HEIGHT / NODE_HEIGHT);

而后,如果咱们再晓得可视区域里的第一个节点在数组中的索引,就能够晓得到底是哪些数据在可视区域了。

而可视区域里的第一个节点的索引,能够通过以后的滚动间隔除以节点高度取得。


// scrollTop 以后的滚动间隔(即以后地位间隔页面顶部的间隔)// start 可视区域的第一个节点的索引
let start = Math.floor(scrollTop / NODE_HEIGHT));

这下,咱们就晓得哪些数据在可视区域了。


let visibleNodes = flatNodes.slice(start, Math.min(start + visibleCount, flatNodes.length));

3.3 模仿滚动

因为咱们实际上只渲染了可视区域的数据,而如果只有这些数据,天然是不能像渲染全副数据时那样能够通过滚动页面来浏览全副数据,所以咱们还须要模仿滚动。

模仿好滚动后,随着滚动,会动静批改可视区域的数据,就能像渲染全副数据时那样通过滚动页面来浏览全副数据了。

为了模仿滚动,咱们须要这样的 HTML 构造:

<div class="virtual-tree" style="height: 500px; overflow: scroll; position: relative;" on-scroll="scrollEvent">
    <div class="tree-phantom" style="height: {{totalHeight}}px;"></div>
    <ul class="visible-tree-nodes" style="position: absolute; top: {{top}}px">
        <s-tree-node s-for="node in visibleNodes" nodeData="{{node}}"></s-tree-node>
    </ul>
 </div>

s-tree-node 是树节点组件,传入树节点的数据即可渲染树节点。

最外层的元素(class 为 virtual-tree)是滚动容器,设置了 overflow: scroll;,它同时也是可视区域,高度是固定的。

滚动容器的第一个子元素(class 为 tree-phantom)是模仿滚动的要害,它是一个占位元素,高度是树的总高度,由节点高度乘节点数量失去。这个占位元素用树的实在高度撑开了滚动容器,因而滚动容器就能够滚动了。

滚动容器的第二个子元素(class 为 visible-tree-nodes),咱们称之为渲染元素,负责渲染咱们计算出来的在可视区域的数据。它是绝对滚动容器(最外层的元素)定位的,初始地位是在滚动容器的顶部。随着容器的滚动,咱们不仅须要如之前所说,更新可视区域的数据,同时,咱们还须要更新渲染元素的垂直偏移量,即下面代码中的 top 的值,不然理当展现在可视区域的数据会跑到可视区域外。


// 滚动时触发的事件函数,负责更新可视区域的数据和更新渲染元素的垂直偏移量
scrollEvent() {
    // 以后的滚动地位
    const scrollTop = ducument.querySelector('virtual-tree').scrollTop;
    // 更新可视区域的第一个节点的索引
    // 在实在实现中,visibleNodes(可视区域的数据)是计算属性,因而会随着 start(可视区域的第一个节点的索引)更新而自动更新
    this.data.set('start', Math.floor(scrollTop / NODE_HEIGHT));

    // 更新渲染元素的偏移量
    this.data.set('top', scrollTop - (scrollTop % NODE_HEIGHT));
}

至此,模仿滚动就实现了。

3.4 把列表装璜成树

到这一步时,虚构树的基本功能咱们都曾经实现了,然而这棵树实质上是个列表,比方没有层级缩进(如之前所示),所以咱们还须要把这个列表装璜成树的样子。

这一步的次要工作是写 CSS,这部分内容就不是明天的重点了,所以咱们略过吧~

4 最终成果

最初,还是 11110 个节点,咱们来看下用虚构树时的渲染耗时:


0.19 秒,比原来的 26 秒快了 99%。

相当震撼。

这个虚构树的性能已在 Santd 的 Tree 组件里实现,有趣味的敌人能够返回 Santd 的官网文档和代码库进一步理解:

  • Santd 官网文档:https://ecomfe.github.io/sant…
  • Santd 代码库:https://github.com/ecomfe/san…

    5 参考资料

  1. 「前端进阶」高性能渲染十万条数据 (虚构列表):https://juejin.cn/post/684490…
  2. 虚构列表 / 树:https://ldc4.github.io/blog/v…
  3. 如何实现一个高性能可渲染大数据的 Tree 组件:https://cloud.tencent.com/dev…

点击进入取得更多技术信息~~

正文完
 0