一文搞懂长列表-虚拟列表实现方案

246 阅读2分钟

一、常见的长列表解决方案

对于长列表,解决方案往往有以下几种:

  1. 分页加载【PC端常用】
  2. 无限列表 vue-infinite-scroll 【掘金就是这样用的】

优点:不需要考虑恢复问题 + 置顶问题,在列表不多的情况下不会存在卡顿的情况;

缺点:随着DOM渲染越来越多,占用内存越多,后面回越来越卡顿

截屏2022-12-02 18.00.41.png 3. 虚拟列表

思想:只渲染可视窗口之内的UI;

优点:流畅,体验感好;

缺点:必须要数据恢复 + 可能存在抖动;

二、虚拟列表

Github Demo在这儿

虚拟列表方案.png 总的来说,虚拟列表的思想也就是只渲染当前用户可以见到的窗口,一般Item分为固定高度和非固定高度两种情况;

<head>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0px;
        }
        .container {
            width: 100%;
            height: 100vh;
            overflow: auto;
        }

        .item {
            background-color: darkgray;
            width: 100%;
            border: thick solid darkgreen;
            text-align: center;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="virtual_list"></div>
    </div>
 </body>
1)固定高度js实现方案
const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => i )
const ITEM_HEIGTH = 100;
const SCROLL_CONTENT_HEIGHT = ITEM_HEIGTH * (dataSource.length); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;
const ITEM_COUNT = Math.ceil(CONTAINER_HEIGHT/ITEM_HEIGTH); // 当前视图show出来的数量

function showCurrentList(target) {
    let scrollTop = target.scrollTop; // 当前视图的scrollTop
    let startIdx = Math.floor(scrollTop / ITEM_HEIGTH);  // 开始值;
    let endIdx = startIdx + ITEM_COUNT;

    let itemList = '';
    const paddingTop = startIdx * ITEM_HEIGTH

    virtualList.setAttribute('style', `padding-top:${paddingTop}px;`)

    for (let i = startIdx; i < endIdx; i++) {
        const element = dataSource[i];
        itemList += `<div class="item" style="height:${ITEM_HEIGTH}px">${element}</div>`
    }
    // TODO: 此处直接用innerHTML,还可以用Document Fragment;
    // 同样是避免单次重复渲染的方式;
    virtualList.innerHTML = itemList;
}

showCurrentList(container)
container.addEventListener('scroll', (event) => {
    showCurrentList(event.target)
})
2)非固定高度js实现方案
const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => { return { idx: i, height: Math.floor(Math.random() * (70 - 0) + 40) } })
const SCROLL_CONTENT_HEIGHT = getScrollContentHeight(dataSource); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;

function getScrollContentHeight(dataSource) {
    let pre = 0;
    dataSource.forEach(item => {
        item.position = pre + item.height;
        pre = item.position; // 记录当前item的终止位置;
    });
    return dataSource[dataSource.length - 1].position;
}

function showCurrentList(target) {
    let scrollTop = target.scrollTop; // 当前视图的scrollTop
    // 找到data中的position大于scrollTop的第一个值 -- startIndex;
    // 找到data中position大于scrollTop + CONTAINER_HEIGHT的第一个值 -- endIndex; 
    // TODO: 寻找startIdx和endIdx存在优化解法,包括二份,Cache缓存等方式;
    let itemList = '';
    let satrt = dataSource.find(e => e.position > scrollTop);
    virtualList.setAttribute('style', `padding-top:${satrt.position - satrt.height}px;`)

    for (let i = satrt.idx; i < dataSource.length; i++) {
        const element = dataSource[i];
        if (element.position > CONTAINER_HEIGHT + scrollTop) break;
        itemList += `<div class="item" style="height:${element.height}px">${element.idx}</div>`
    }
    virtualList.innerHTML = itemList;
}

showCurrentList(container)
container.addEventListener('scroll', (event) => {
    showCurrentList(event.target)
})
3)固定高度 + 缓冲区

为了避免上下滑动的时候,出现卡顿白屏等加载不及时的情况,给上下区域都增加一个缓冲区padding;

【如果滑动的实在太快,其实卡顿白屏的情况其实是难以避免的】

const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => i )
const ITEM_HEIGTH = 100;
const SCROLL_CONTENT_HEIGHT = ITEM_HEIGTH * (dataSource.length); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;
const ITEM_COUNT = Math.ceil(CONTAINER_HEIGHT/ITEM_HEIGTH); // 当前视图show出来的数量
const PADDING_ITEM = 5

function showCurrentList(target) {
    let scrollTop = target.scrollTop; // 当前视图的scrollTop
    let startIdx = Math.floor(scrollTop / ITEM_HEIGTH);  // 开始值;
    let endIdx = startIdx + ITEM_COUNT + PADDING_ITEM;

    let itemList = '';
    const paddingTop = startIdx * ITEM_HEIGTH

    virtualList.setAttribute('style', `padding-top:${paddingTop}px;`)
    startIdx = startIdx - PADDING_ITEM > 0 ? startIdx - PADDING_ITEM : startIdx
    endIdx = endIdx > dataSource.length ? dataSource.length : endIdx

    for (let i = startIdx; i < endIdx; i++) {
        const element = dataSource[i];
        itemList += `<div class="item" style="height:${ITEM_HEIGTH}px">${element}</div>`
    }
    virtualList.innerHTML = itemList;
}

showCurrentList(container)
container.addEventListener('scroll', (event) => {
    showCurrentList(event.target)
})
4)CSS新属性

content-visibility:auto;

主流的Chrome和Edge支持,但不支持的浏览器还是很多。

作用:如果该元素不在屏幕上,并且与用户无关,则不会渲染其后代元素。

三、可以直接用的npm包

Vue: vue-virtual-scroll-list

React: react-window

四、存在问题:

  1. 滚动太快会出现白屏

    增加滚动缓存区【其实如果太快还是会卡顿了,但这个没办法, 而且其实这种问题用户也不是不可以接受,很多app都是变下拉便拉数据,无论如何都可以白屏的】;

  2. DOM过多 + 网络请求仍旧会卡

    配合骨架屏食用