列表虚拟滚动 - 详解

253 阅读1分钟

是什么:

20210226141203166.png

列表虚拟滚动:就是三个盒子外层为视窗, 中间层为整个数据列表的高度盒子没有渲染任何数据只是做撑开高度和滚动的作用, 内层盒子进行数据渲染和展示并且滚动的时候内层盒子不断的替换数据并且模拟滚动效果;

为什么:

性能优化

怎么做

代码实现

// vue进行实现:
<template>
    <div :style="`height:${viewH}px;overflow-y:scroll`" @scroll="handleScroll" class="out">
        <div :style="`height:${scrollH}px`" class="list">
            <div class="item_box" :style="`transform:translateY(${offsetY}px)`">
                <div class="item"
                     :style="`height: ${itemH}px`"
                     v-for="(item, index) of list"
                     :key="index">
                    {{ item }}
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "virtualList",
        props: {
            data: Array,   // 列表总数据
            viewH: Number, // 外部高度
            itemH: Number, // 单项高度
        },
        data() {
            return {
                scrollH: '', // 整个滚动列表高度
                list: [],    // 每次显示的数据
                showNum: '',
                offsetY: '',// 动态偏移量- 外层的盒子进行滚动设置
                lastTime: '',
            }
        },
        mounted() {
            // 初始化计算
            this.scrollH = this.data.length * this.itemH;
            // 计算可视化高度一次能装几个列表, 多设置几个防止滚动时候直接替换
            this.showNum = Math.floor(this.viewH / this.itemH) + 4;
            // 默认展示几个
            this.list = this.data.slice(0, this.showNum);
            this.lastTime = new Date().getTime();
        },
        methods: {
            handleScroll(e) {
                if (new Date().getTime() - this.lastTime > 10) {
                    let scrollTop = e.target.scrollTop; // 滚动条的宽度
                    // 每一次滚动后根据scrollTop值获取一个可以整除itemH结果进行偏移
                    // 例如: 滚动的scrllTop = 1220  1220 % this.itemH = 20  offsetY = 1200
                    this.offsetY = scrollTop - (scrollTop % this.itemH);
                    console.log(scrollTop, scrollTop % this.itemH);
                    this.list = this.data.slice(
                        Math.floor(scrollTop / this.itemH),  // 计算卷入了多少条
                        Math.floor(scrollTop / this.itemH) + this.showNum
                    )
                    this.lastTime = new Date().getTime();
                }
            }
        }
    }
</script>
// 类实现方法
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #list {
            border: 1px solid #333333;;
            overflow-y: scroll;
        }

        .listBox {
            border: 1px solid #00B7FF;
        }
    </style>
</head>
<body>
<div id="list"></div>
</body>
<script>
    /*
    * 配置项:
    * el: 钩子
    * data: 所有列表项
    * viewH: 列表高度
    * itemH: 每项高度
    * list: 当前展示项
    * showNum: 一次展示多少个
    *
    * */
    class List {
        constructor(opt) {
            this.el = opt.el;
            this.data = opt.data;
            this.viewH = opt.viewH;
            this.itemH = opt.itemH;
            this.totalH = this.data.length * this.itemH + 'px';
            this.list = [];
            this.offsetY = 0;
            this.showNum = 0;
            this.lastTime = new Date().getTime();

            this.el.style.height = this.viewH + 'px';
        }

        init() {
            this.composeDom();
            this.initData();
            this.bindEvent();
        }

        composeDom() {
            // 建立dom结构
            let totalBox = document.createElement('div'),
                listBox = document.createElement('div');
            totalBox.className = 'totalBox';
            totalBox.style.height = this.totalH;
            listBox.className = 'listBox';
            totalBox.append(listBox);
            this.el.append(totalBox);
            this.totalBox = totalBox;
            this.listBox = listBox;
        }

        initData() {
            // 初始化列表
            this.showNum = Math.floor(this.viewH / this.itemH) + 4;
            this.list = this.data.slice(0, this.showNum);
            this.createByList(this.list);

        }

        createByList(list) {
            this.listBox.innerText = '';
            let fragment = document.createDocumentFragment();
            for (let i = 0; i < this.showNum; i++) {
                let div = document.createElement('div');
                div.style.height = this.itemH + 'px';
                div.innerText = list[i];
                fragment.append(div);
            }
            this.listBox.append(fragment);
        }

        bindEvent() {
            this.el.addEventListener('scroll', this.handleScroll.bind(this), false);
        }

        handleScroll(e) {
            if (new Date().getTime() - this.lastTime > 20) {
                let scrollTop = e.target.scrollTop,
                    prevNum = Math.floor(scrollTop / this.itemH);
                this.offsetY = scrollTop - (scrollTop % this.itemH);
                this.list = this.data.slice(prevNum, prevNum + this.showNum);
                this.createByList(this.list);
                this.listBox.style.transform = `translateY(${this.offsetY}px)`
                this.lastTime = new Date().getTime();
            }

        }
    }


    let data = [];
    for (let i = 0; i < 999; i++) {
        data.push(i + 1);
    }


// 实例进行挂载
    new List({
        el: document.getElementById('list'),
        data,
        viewH: 600,
        itemH: 40
    }).init();
</script>
</html>

总结: 其实就是scrollTop的运用以及滚动的事件处理, 理解了其中的逻辑实现起来还是很简单的