前端列表优化-虚拟滚动

2,273 阅读1分钟

最近票务项目一次性返回数据太多了, 我的电脑还扛得住但是看到客户的电脑顿时炸了... 这不是又要去研究一下列表数据优化问题了

1. 问题总汇

  • 后端一次性返回数据太多直接渲染造成性能卡顿
  • 稍微差点的电脑对大量数据dom渲染容易卡屏死机
  • 函数节流和防抖的优化基本不起作用的情况下该如何对列表进行优化

2. 虚拟滚动图示

其实就是监听滚动事件不断的对已经存在的列表进行数据更新, 如图:

GIF1.gif

仔细看控制台其实列表div的个数没有变化只是变化的是里面的数据.

clipboard.png

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

3. 代码实现

分vue和原生js进行实现 可直接拷贝使用

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>

4. 总结知识点

就是对scrollTop的使用, 如何根据卷入的高度计算已经卷入了多少条数据, 来展示之后的数据. 嗯其实很简单,但是自己也研究一下午哎, 现在还在加班,没事做就把今天遇到的问题写出来吧, 希望能帮助大家. ヾ(◍°∇°◍)ノ゙