虚拟列表实现

1,839 阅读2分钟

虚拟列表:简而言之,对于大量列表数据,仅渲染滚动区域内可视区域 所对应 数据源 的部分数据

举个例子:

比如我要渲染1000条数据,每条数据高度60px,如果我想把全部元素渲染,那么滚动区域至少需要6000px高但是呢,如果我渲染1000条数据,那么整个dom树就会出现巨量dom元素,如下图,B,C,D区域将全部都是dom元素,一旦回流重绘就会非常卡顿。

image.png

这种场景的优化方案就是采取虚拟列表去渲染,即我们要做的就是在滚动的时候只渲染可视区域对应的dom元素,即下图的B,非可视区域内元素不去渲染(即C,D区域不渲染元素),这样的话可以减少大量非必要DOM渲染(C,D区域用户也不需要看见,所以它的dom渲染是非必要的),去提高性能!

虚拟列表实现原理:根据视口高度(上图中的B区域高度),父元素的滚动距离(scrollTop)及每一个列表元素item的平均高度,计算出当前scrollTop到scrollTop+视口高度这段可视区域 对应数据源列表所需要展示的 部分数据,再将这部分数据展示在B区域内,对于隐藏滚动区域C,D使用paddingTop&&paddingBottom填充,保持整个滚动区域高度不变,同时监听滚动,滚动触发时动态改变可是区域内数据及paddingTop&&paddingBottom等数据:

  • 1,监听容器元素滚动

  • 2,获取容器元素滚动距离scrollTop

  • 3,获取当前滚动距离(scrollTop)下可视区域首个需要渲染元素索引(in 数据源)startIndex,计算方式:startIndex = 滚动距离(scrollTop)/ 列表元素高度(itemHeight)

  • 4,获取当前滚动距离(scrollTop)下可视区域最后一个需要渲染元素索引(in 数据源)endIndex,计算方式:endIndex = startIndex + 可视区域高度(viewHeight)/ 列表元素高度(itemHeight)

  • 5,根据startIndex和endIndex获取数据源对应的 可视区域需要展示的数据viewData,计算方式:viewData =dataSource(数据源).slice(startIndex,endIndex+1)

  • 6,计算C区域的高度,既paddingTop,计算方式:paddingTop = startIndex*itemHeight(列表元素高度)

  • 7,计算D区域的高度,既paddingBottom,计算方式:paddingBottom = contentHeight(整个滚动区域高度,既A区域)-paddingTop-可视区域高度(viewHeight)

  • 8,清除可是区域内上一次展示的数据,使用新数据viewData重新填充,且设置新的paddingTop&paddingBottom

代码实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
        }

        #container {
            width: 100%;
            height: 100%;
        }

        #content {
            background-color: pink;
        }
    </style>
</head>

<body>
    <div id="container">
        <div id="content"></div>
    </div>
    <script>

        function createVlist(container, content, data_Source, item_Height) {
            const dataSource = data_Source  // 数据源
            const itemHeight = item_Height  // 每一行元素高度
            const viewHeight = container.clientHeight // 视口高度(虚拟列表所展示部分高度)
            const contentHeight = itemHeight * dataSource.length // 滚动区域高度
            const itemCount = Math.ceil(viewHeight / itemHeight) // 视口元素数量
            // 设置容器元素overflow: auto (生成滚动区域,不被截取)
            container.setAttribute('style', `overflow:auto`)
            // 设置滚动区域高度
            content.setAttribute('style', `height:${contentHeight}px`)
            const scrollCallback = e => {
                const scrollTop = e.target.scrollTop                 // 容器元素的滚动距离
                const startIndex = Math.ceil(scrollTop / itemHeight) // 视口第一个元素所在数据源中的索引
                const endIndex = startIndex + itemCount              // 视口最后一个元素所在数据源中的索引
                // 根据startIndex endIndex找出数据源中需要展示在虚拟列表中的部分数据
                const itemList = dataSource.slice(startIndex, endIndex + 1)

                // 滚动区域高度contentHeight =  滚动区域paddingTop + 视口高度 + 滚动区域paddingBottom
                const paddingTop = startIndex * itemHeight
                const paddingBottom = contentHeight - paddingTop - itemCount * itemHeight

                // 动态调整滚动区域的paddingTop  paddingBottom 保证列表部分始终展示在视口
                content.setAttribute('style', `padding-top:${paddingTop}px;padding-bottom:${paddingBottom}px`)
                // 展示下一批次列表前删除上一批次列表数据
                content.innerHTML = ''
                // 动态调整每次滚动后对应的展示数据
                for (const val of itemList) {
                    const item = document.createElement('div')
                    item.innerHTML = val
                    item.setAttribute('style', `background-color:${val % 2 === 0 ? 'red' : 'blue'};width:100%;height:${item_Height}px`)
                    content.appendChild(item)
                }
            }
            // 添加容器元素的滚动监听
            container.addEventListener('scroll', scrollCallback)
            // 初始首屏数据
            scrollCallback({ target: container })
        }
        // 创建虚拟列表
        createVlist(container, content, Array.from({ length: 100 }, (v, i) => i), 60)

    </script>
</body>

</html>