后端一次性返回千万级数据怎么办?虚拟列表给出答案

1,397 阅读2分钟

首先我没遇到过这种需求,只是图一乐而已,顺便锻炼一下能力。

不过就算上百条数据用这方法也能提升性能,所以我不能不会啊。

思路

  • 准备容器,设置overflow: auto
  • 子容器装着固定长度的数据,防止内存溢出
  • 滚动时,实时计算子容器数据,设置scroll
  • 设置节流避免频繁计算

实现

先看HTML

.container {
    overflow: auto;
    margin: 10px;
}

ul {
    background-color: lightcoral;
}

<div class="container">
</div>

就这点就够了,我们使用JS来实现

// 后端数据
const data = new Array(9999999).fill(0).map((_, i) => i);
// 配置
const bef = 10, // 前后可多查看的条数
    aft = 10,
    container = document.querySelector('.container'),
    containerHeihgt = 500,
    perHeight = 30,
    totalHeight = perHeight * data.length;
// 可视数量
const num = Math.ceil(containerHeihgt / perHeight);

let st = 0,
    end = num;

首先准备配置文件

function createDOM() {
    const newData = data.slice(st, end + 1),
        lis = newData.map((item) => `<li style="height: ${perHeight}px">${item}</li>`);

    const innerHTML = `<ul style="height: ${totalHeight}px">${lis.join('')}</ul>`;
    container.innerHTML = innerHTML;
}

截取可视数据,放入新数组,生成DOM

接下来是重点了,怎么让滚动时,数据跟着变化呢,首先直接绑定个事件先

container.onscroll = onScroll;

那么接下来我们要做什么呢?

  • 求出滚动时产生的数据,与界面显示的关系
  • 根据数据关系,算出起始/ 结束索引
  • 设置ul的位置,让他一直处于容器可视位置

ok,接下来我们来实现

function onScroll() {
    const [stIndex, endIndex] = getIndex(this.scrollTop);
    setData(stIndex, endIndex);
    setPos();
}

我们只要实现这三个函数,即可完成

获取索引

function getIndex(top) {
    end = Math.ceil((top + containerHeihgt) / perHeight) + aft;
    st = Math.floor(top / perHeight) - bef;

    return [st, end];
}

(超出距离 + 容器高度) / 每一项高度 = 结束索引

超出距离 / 每一项高度 = 开始索引

再把配置文件的前后可多查看的条数加上

有可能用户滑动的距离,正好超过了一内内,所以end结束索引用天花板函数,st反之用地板砖函数

来看看有没有问题

image.png

为什么索引变成负数呢?

这时因为我们配置可超出视距范围参数bef & aft

我开头设置的是10

所以当滚动触发时,如果st < 10,就会变成负数,只需要加个判断即可

st < 0 && (st = 0)

来看下一个问题

image.png

这个4是可见的,但是起始索引却是5

为什么啊??我不是向下取整了吗?

我们到开发者工具仔细看看

腚眼一看,噢,源濑市margin导致的

image.png

我们把margin设置为0,再来看看页面,是不是只能看见5

image.png

所以说,初始化样式真的很重要,能让你少debugger很多时间

设置数据

function setData(stIndex, endIndex) {
    st = stIndex;
    end = endIndex;

    createDOM();
}

上面有了索引,所以我们改动开头的索引,重复调用createDOM即可,这就是低耦合度的好处。

调用后只是重置了数据,但是滚动后位置还没设置呢

设置位置

function setPos() {
    const ul = container.querySelector('ul');
    ul.style.transform = `translateY(${(st) * perHeight}px)`;
}

只需要用CSStransform改动一下Y轴即可

ok,大功告成

msedge_jrHl7dsV8e.gif

可以看到,非常丝滑,DOM数量也是固定的,不会因为节点过多影响性能

还有一个问题,触发太频繁了看到了吗,滑动时div一直闪烁,因为一直在重新替换元素

所以还是传统艺能,节流

function throttle(fn, duration = 50) {
    let st = Date.now();

    return function () {
        const now = Date.now();
        if (now - st >= duration) {
            st = now;
            return fn.apply(this, arguments);
        }
    };
}

container.onscroll = throttle(onScroll);

源码 gitee.com/cjl2385/dig…