JavaScript原生实现简单虚拟列表(列表不定高)

作者:重庆崽儿Brand

前言

之前实现了一个定高版本的虚拟列表,今天在定高版本的基础上稍作调整,来实现不定高版本,之前的版本请跳转对应文章查看:JavaScript原生实现简单虚拟列表(固定高度)

先说结论

实现不定高的原理就是:每次把内容渲染到页面后,都去重新获取一次 item 的实际高度,然后再执行一次渲染

最开始想用 createDocumentFragment 创建一个虚拟的文档节点,先将内容渲染到这个虚拟的文档节点中,最后从虚拟文档节点中获取 item 的实际高度,最后,一次插入到真实DOM中。结果发现,在虚拟的文档节点中时拿不到 item 的实际高度的,所以才有了下面的实现方式。

完整代码

不定的高版本代码如下:

html 和 css 与定高版本相比,未做任何调整

    <div class="container">
        <div class="zhanwei"></div>
    </div>
      <style> 
        .container {
            border: 1px solid #eee;
            height: 300px;
            width: 300px;
            overflow: auto;
            position: relative;
            box-sizing: border-box;
        }
        .zhanwei {
            position: relative;
        }
        .item {
            position: absolute;
            top: 0;
            min-height: 50px;
            width: 100%;
            border: 1px solid #eee;
            will-change: transform; 
            box-sizing: border-box;
        }
        .item:nth-of-type(odd) {
            background: #00ccff;
            }

            .item:nth-of-type(even) {
            background: #ffcc00;
            }
    </style>

js 代码如下

和定高版本相比,就两处改动,请查看代码中的 变化一变化二

// 不固定高度版本
        let container = document.querySelector('.container');
        let zhanwei = document.querySelector('.zhanwei');

        let itemList = []; // 假设有10000条数据

        for (let i = 0; i < 10000; i++) {// 生成10000条数据
            itemList.push({
                index: i,
                content: `Item ${i} - ${"Hello world!".repeat(Math.floor(Math.random() * 10))}`
            });
        };
        let buffer = 5; // 多渲染几条,避免滚动看着异常

        let itemHeight = 50; // 每条数据的一个默认最小高度

        let heights = new Map();// 记录渲染的每个 item 的高度,为不定高版本做准备
        let offsets = new Map(); // 记录每个 item 的偏移量,即每个item距离顶部的距离
        let rendered = new Map(); // 存储已渲染的数据
        

        // 更新偏移量, 根据item高度,计算 zhanwei 元素的高度,好让container出现滚动条
        function updateOffsets() {
            let offset = 0
            for (let i = 0; i < itemList.length; i++) {
                let h = heights.get(i) ?? itemHeight; // ?? 是空值合并运算符,当左边为null或者undefined时使用右边值,和三元运算符相比,排除了 0 的干扰
                offsets[i] = offset;
                offset += h + 5; // 加上了5个像素的间距
                
            }
            zhanwei.style.height = offset + 'px';
        }

        // 变化一:创建一个重新渲染函数
        function rerender(item, i) {
            let height = item.getBoundingClientRect().height
            if (heights.get(itemList[i].index) !== height) {
                heights.set(itemList[i].index, height)
                updateOffsets()
                render()
            }
        }
        // 渲染数据
        function render() {
            let scrollTop = container.scrollTop;
            let viewHeight = container.clientHeight;

            let start = 0; // 查找视口第一个item的索引
            while (start < itemList.length && offsets[start + 1] < scrollTop) {
                start++;
            }
            let end = start ;// 查找视口最后一个item的索引
            while (end < itemList.length && offsets[end] < scrollTop + viewHeight) {
                end++;
            }
            start = Math.max(0, start - buffer);
            end = Math.min(itemList.length, end + buffer);
            let nextRendered = new Map(); // 当前需要渲染的数据
            for (let i = start; i < end; i++) {
                if (!rendered.has(i)) {
                    let item = document.createElement('div')
                    item.className = 'item'
                    item.style.transform = `translateY(${offsets[itemList[i].index] + 'px'})`
                    item.textContent = itemList[i].content
                    container.appendChild(item)
                    rendered.set(i, item)

                    // 变化二:向页面插入数据后执行一次重新渲染
                    rerender(item, i)
                }
                nextRendered.set(i, rendered.get(i))
                
            }
            // 不可见的区域 移除
            for (const [i, el] of rendered.entries()) {
                if (!nextRendered.has(i)) {
                 container.removeChild(el);
                }
            }
            // 更新 rendered
            rendered.clear();
            for (const [i, el] of nextRendered.entries()) {
                rendered.set(i, el);
            }

        }
        container.addEventListener("scroll", render);
        updateOffsets()
        render()

如果你有更好的实现方案,欢迎留言、贴代码交流。

感谢你的阅读 ❤️

文章归类于: 码不停蹄

文章标签: #Javascript#项目

版权声明: 自由转载-署名-非商用