列表虚加载

2022-11-15  本文已影响0人  欢西西西
<div class="box" id="box">
     <ul id="ul"></ul>
 </div>
.box {
    height: 500px;
    width: 400px;
    border: 2px solid blue;
    text-align: center;
    overflow: auto;
    padding-top: 20px;
    margin-top: 50px;
}

#ul {
    margin: 0;
    list-style: none;
    padding-left: 0;
}

#ul li {
    margin: 10px 5px 10px;
    padding: 5px 0 10px 0;
    border: 1px solid #000;
}   
class VirtualScroll {
    constructor({ scrollBox: box,
        listBox: ul,
        renderCount = 20, // 每次【最多】渲染N个,这里每批渲染的数量应该大于preDraw+每屏最多能放的卡片个数,不能设置太小
        list, // 全部数据
        onUptList, // 滚动时内部计算要渲染的数据传给onUptList
        onReachEnd // 滚动到最底触发onReachEnd
    }) {
        this.preDraw = 5; // 在可视范围显示的第一个卡片前面再展示N个
        this.btmConcatCount = 5; // 如果在触底后增加数据,则往下渲染N个
        this._resetProp();

        this.box = box;
        this.ul = ul;
        this.shift = ul.offsetTop;
        this.renderCount = renderCount;
        this.list = list;
        this.onUptList = onUptList;
        this.onReachEnd = onReachEnd;
        this._bindEvent();
        if (list.length) {
            this._onScroll();
        }
    }
    _resetProp() {
        this.itemHeights = []; // 存每个卡片到顶部占的高度
        this.startIndex = null;
        this.endIndex = null; // 不包含
        clearTimeout(this.scrollTimer);
        this.scrollTimer = null;
    }
    // 计算要渲染的数据应从哪项开始
    _getStartIndexOnScroll(scrollTop, arrHeight) {
        if (!arrHeight.length) {
            return 0;
        }
        let cur;
        for (let i = 0, lth = arrHeight.length; i < lth; i++) {
            if (scrollTop < arrHeight[i]) {
                cur = i;
                break;
            }
        }
        return Math.max(cur - this.preDraw + 1, 0);
        console.error('没找到合适的index');
        console.log('scrollTop:', scrollTop, arrHeight);
    }
    // 计算应渲染的数据 
    _getDrawList(allData, startIndex, endIndex) {
        let items = [];
        while (startIndex < endIndex) {
            let cur = allData[startIndex];
            items.push(cur);
            startIndex++;
        }
        return items;
    }
    _bindEvent() {
        this.box.addEventListener('scroll', () => {
            if (this.scrollTimer) {
                return;
            }
            this.scrollTimer = setTimeout(() => {
                this._onScroll();
                clearTimeout(this.scrollTimer);
                this.scrollTimer = null;
            }, 250);
        });
    }
    _isReachBtm() {
        let box = this.box;
        return box.scrollHeight - box.clientHeight - box.scrollTop < 50;
    }
    _onScroll() {
        let scrollTop = this.box.scrollTop;
        let box = this.box, list = this.list, itemHeights = this.itemHeights;

        let start = this._getStartIndexOnScroll(scrollTop, itemHeights);

        let calcCardsCount = itemHeights.length;

        if (start === this.startIndex) {
            if (calcCardsCount && this._isReachBtm() && calcCardsCount === list.length) { // 触底
                return this.onReachEnd();
            }
            return;
        }

        this.startIndex = start;
        this.endIndex = Math.min(list.length, start + this.renderCount);

        this._updateDrawList();

        calcCardsCount = itemHeights.length;
        if (calcCardsCount && this._isReachBtm()) { // 触底
            if (calcCardsCount === list.length) {
                return this.onReachEnd();
            }
        }
    }
    // 计算完startIndex和endIndex来更新列表
    _updateDrawList() {
        let start = this.startIndex,
            end = this.endIndex,
            itemHeights = this.itemHeights,
            calcCardsCount = itemHeights.length;

        let topSpace = start > 0 ? itemHeights[start - 1] - this.shift + 'px' : 0,
            bottomSpace = end < calcCardsCount ? itemHeights[calcCardsCount - 1] - itemHeights[end] + 'px' : 0;
        this.ul.style.paddingTop = topSpace;
        this.ul.style.paddingBottom = bottomSpace;

        // 传给调用者自己更新列表,这里应该同步更新,因为下一步可能要存height
        this.onUptList({
            list: this._getDrawList(this.list, start, end),
            topSpace,
            bottomSpace
        });

        if (calcCardsCount < end) { // 之前没有存过height的再存
            let i = start;
            Array.from(this.ul.childNodes).forEach((node) => {
                if (itemHeights[i] === undefined) {
                    itemHeights[i] = node.offsetHeight + node.offsetTop;
                }
                i++;
            });
        }
    }
    setData(list) { // 重新设置数据
        this._resetProp();
        this.list = list || [];
        if (!this.list.length) {
            return this.onUptList({
                list: [], topSpace: 0, bottomSpace: 0
            });
        }
        if (this.box.scrollTop > 0) {
            this.box.scrollTo(0, 0);
        } else {
            this._onScroll();
        }
    }
    addData(list) { // 拼接数据
        if (!list || !list.length) {
            return;
        }
        if (!this.list.length) {
            this.list = list;
            return this._onScroll();
        }
        this.list = this.list.concat(list);
        if (this._isReachBtm()) {
            this.endIndex = Math.min(this.list.length, this.startIndex + this.renderCount + this.btmConcatCount);
            this._updateDrawList();
        }
    }
}
// 生成假数据
function getMock(count) {
    let data = [], minHeight = 30;
    for (let i = 0; i < count; i++) {
        data[i] = {
            height: parseInt(minHeight + Math.random() * 100), // 随机行高
            // height: 30,
            color: `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`,
            text: i + 1
        }
    }
    return data;
}

// 如果每个li的上下margin不一样,这里滚动时也会抖动
let ul = document.getElementById('ul');


const v = new VirtualScroll({
    scrollBox: document.getElementById('box'),
    listBox: ul,
    renderCount: 15,
    list: getMock(50),
    onUptList({ list }) {
        ul.innerHTML = list.map(cur => `<li style='background-color: ${cur.color};line-height:${cur.height}px;'>
            注意卡片是否抖动:${cur.text} </li>`).join('');
    },
    onReachEnd() {
        console.log('加载下一页');
    }
});
上一篇 下一篇

猜你喜欢

热点阅读