requestAnimationFrame,虚拟滚动,大数据渲染页面不卡顿

requestAnimationFrame 是一个用于在浏览器的下一次重绘之前执行回调函数的方法。它利用浏览器的帧同步机制,在每一帧开始之前执行指定的回调函数,以保证在合适的时机进行绘制和动画操作

虚拟滚动(Virtual Scrolling)技术,只渲染当前可见的部分数据,而不是将所有数据一次性加载到页面中。这样可以提高性能,避免页面卡顿。

如何渲染几万条数据并不卡住界面

这道题考察了如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次

<!DOCTYPE html>
<html>

<body>
    <div id="container"></div>
    <script>
        // 生成几万条模拟数据
        const data = [];
        for (let i = 0; i < 1000000; i++) {
            data.push(`Item ${i + 1}`);
        }
        const container = document.getElementById('container');
        const batchSize = 100; // 每批次渲染的数量
        let currentBatch = 0; // 当前批次
        function renderBatch() {
            const start = currentBatch * batchSize; // 计算当前批次的起始索引
            const end = Math.min(start + batchSize, data.length); // 计算当前批次的结束索引
            for (let i = start; i < end; i++) {
                const item = document.createElement('div');
                item.className = 'item';
                item.innerText = data[i];
                container.appendChild(item);
            }
            currentBatch++; // 更新当前批次
            if (currentBatch * batchSize < data.length) {
                // 如果还有更多数据需要渲染,使用 requestAnimationFrame 进行下一批次的渲染
                requestAnimationFrame(renderBatch);
            }
        }
        renderBatch(); // 开始第一批次的渲染
    </script>
</body>

</html>

使用js实现一个持续的动画效果

//兼容性处理
window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame    ||
           function(callback){
                window.setTimeout(callback, 1000 / 60);
           };
})();

var e = document.getElementById("e");
var flag = true;
var left = 0;

function render() {
    left == 0 ? flag = true : left == 100 ? flag = false : '';
    flag ? e.style.left = ` ${left++}px` :
        e.style.left = ` ${left--}px`;
}

(function animloop() {
    render();
    requestAnimFrame(animloop);
})();

打基础

<template>
	<div>
		<button v-on="{click:fn}">点击按钮</button> // 在这里给dom绑定了原生的click
		// 当点击按钮 在控制台会打印出 11111
	</div>
</template>
// 在methods内声明的函数
fn(){
    console.log(11111);
}

实现vue虚拟滚动插件

VirtualBlock.vue

<template>
    <div v-on="pageMode ? {} : {scroll: handleScroll}" :style="containerStyle" ref="vb">
        <div :style="{height: `${offsetTop}px`}"></div>
        <div v-for="item in renderList" 
             :style="{height: `${fixedBlockHeight ? fixedBlockHeight : item.height}px`}" 
             :key="`${item.id}`">
            <slot :data="item"></slot>
        </div>
        <div :style="{height: `${offsetBot}px`}"></div>
    </div>
</template>

<script>
export default {
    props: {
        data: {
            type: Array,
            required: true
        },
        height: {
            type: Number
        },
        fixedBlockHeight: {
            type: Number
        },
        pageMode: {
            type: Boolean,
            default: true
        }
    },
    data() {
        return {
            viewportBegin: 0,
            viewportEnd: this.height,
            offsetTop: 0,
            offsetBot: 0,
            renderList: [],
            transformedData: []
        }
    },
    watch: {
        data: {
            handler: function(newVal, oldVal) {
                this.computeTransformedData(newVal);
                if (oldVal) {
                    this.$nextTick(
                        () => {
                            this.$refs.vb.scrollTop = 0;
                            this.handleScroll();
                        }
                    );
                }
            },
            immediate: true
        },
        pageMode(newVal) {
            if (newVal) {
                window.addEventListener('scroll', this.handleScroll);
            } else {
                window.removeEventListener('scroll', this.handleScroll);
            }
            this.computeTransformedData(this.data);
            this.$nextTick(
                () => {
                    this.$refs.vb.scrollTop = 0;
                    this.handleScroll()
                }
            );
        },
        fixedBlockHeight() {
            this.handleScroll();
        }
    },
    mounted() {
        if (this.pageMode) {
            window.addEventListener('scroll', this.handleScroll);
            this.computeTransformedData(this.data);
        }
        this.updateVb(0);
    },
    destroyed() {
        if (this.pageMode) {
            window.removeEventListener('scroll', this.handleScroll);
        }
    },
    methods: {
        computeTransformedData(oriArr) {
            if (!this.fixedRowHeight && ((this.pageMode && this.$refs.vb) || !this.pageMode)) {
                let curHeight = this.pageMode ? this.$refs.vb.offsetTop : 0;
                let rt = [curHeight];
                oriArr.forEach(
                    item => {
                        curHeight += item.height;
                        rt.push(curHeight);
                    }
                );
                this.transformedData = rt;
            }
        },
        handleScroll() {
            const scrollTop = this.pageMode ? window.scrollY : this.$refs.vb.scrollTop;
            window.requestAnimationFrame(
                () => {
                    this.updateVb(scrollTop);
                }
            );
        },
        binarySearchLowerBound(s, arr) {
            let lo = 0;
            let hi = arr.length - 1;
            let mid;
            while(lo <= hi) {
                mid = ~~((hi + lo) / 2);
                if (arr[mid] > s) {
                    if (mid === 0) {
                        return 0;
                    } else {
                        hi = mid - 1;
                    }
                } else if (arr[mid] < s) {
                    if (mid + 1 < arr.length) {
                        if (arr[mid + 1] > s) {
                            return mid;
                        } else {
                            lo = mid + 1;
                        }
                    } else {
                        return -1;
                    }
                } else {
                    return mid;
                }
            }
        },
        binarySearchUpperBound(e, arr) {
            let lo = 0;
            let hi = arr.length - 1;
            let mid;
            while(lo <= hi) {
                mid = ~~((hi + lo) / 2);
                if (arr[mid] > e) {
                    if (mid > 0) {
                        if (arr[mid - 1] < e) {
                            return mid;
                        } else {
                            // normal flow
                            hi = mid - 1;
                        }
                    } else {
                        return -1;
                    }
                } else if (arr[mid] < e) {
                    if (mid === arr.length - 1) {
                        return arr.length - 1;
                    } else {
                        lo = mid + 1;
                    }
                } else {
                    return mid;
                }
            }
        },
        fixedBlockHeightLowerBound(s, fixedBlockHeight) {
            const sAdjusted = this.pageMode ? s - this.$refs.vb.offsetTop : s;
            const computedStartIndex = ~~(sAdjusted / fixedBlockHeight);
            return computedStartIndex >= 0 ? computedStartIndex : 0;
        },
        fixedBlockHeightUpperBound(e, fixedBlockHeight) {
            const eAdjusted = this.pageMode ? e - this.$refs.vb.offsetTop : e;
            const compuedEndIndex = Math.ceil(eAdjusted / fixedBlockHeight);
            return compuedEndIndex <= this.data.length ? compuedEndIndex : this.data.length;
        },
        findBlocksInViewport(s, e, heightArr, blockArr) {
            var vbOffset = this.pageMode ? this.$refs.vb.offsetTop : 0;
            const lo = this.fixedBlockHeight ? 
                        this.fixedBlockHeightLowerBound(s, this.fixedBlockHeight) :
                        this.binarySearchLowerBound(s, heightArr);
            const hi = this.fixedBlockHeight ? 
                        this.fixedBlockHeightUpperBound(e, this.fixedBlockHeight) :
                        this.binarySearchUpperBound(e, heightArr);

            if(this.fixedBlockHeight) {
                this.offsetTop = lo >= 0 ? lo * this.fixedBlockHeight : 0;
                this.offsetBot = hi >= 0 ? (blockArr.length - hi ) * this.fixedBlockHeight : 0;
            } else {
                this.offsetTop = lo >= 0 ? heightArr[lo] - vbOffset : 0;
                this.offsetBot = hi >= 0 ? heightArr[heightArr.length - 1] - heightArr[hi] : 0;
            }
            return blockArr.slice(lo, hi);
        },
        updateVb(scrollTop) {
            const viewportHeight = this.pageMode ? window.innerHeight : this.height;
            this.viewportBegin = scrollTop;
            this.viewportEnd = scrollTop + viewportHeight;
            this.renderList = this.findBlocksInViewport(this.viewportBegin, this.viewportEnd, this.transformedData, this.data);
        }
    },
    computed: {
        containerStyle() {
            return {
                ...(!this.pageMode && {height: `${this.height}px`}),
                ...(!this.pageMode && {'overflow-y' : 'scroll'})
            }
        }
    }
}
</script>

在App.vue内使用

<template>
    <div>
        <select v-model="dataAmt">
            <option value="100">100</option>
            <option value="1000">1000</option>
            <option value="10000">10000</option>
        </select>
        <input type="checkbox" v-model="isPageMode">
        <input type="checkbox" v-model="isFixedHeight">

        <VirtualBlock :fixedBlockHeight="isFixedHeight ? 50 : undefined"  :pageMode="isPageMode" :height="500" :data="data" ref="vb">
            <template slot-scope="{data}">
                <div :style="{height: '100%', 'background-color': data.color}">
                    {{data.id}}
                </div>
            </template>
        </VirtualBlock>
    </div>
</template>

<script>
export default {
    name: "App",
    data() {
        return {
            dataAmt: '1000',
            isPageMode: false,
            data: [],
            isFixedHeight: true
        }
    },
    created() {
        this.data = this.dataConstructor(this.dataAmt, this.isFixedHeight);
    },
    watch: {
        dataAmt: function() {
            this.data = this.dataConstructor(this.dataAmt, this.isFixedHeight);
        },
        isFixedHeight: function() {
            this.data = this.dataConstructor(this.dataAmt, this.isFixedHeight);
        }
    },
    methods: {
        dataConstructor(amount, fixedHeight) {
            let arr = [];
            for (let i = 0 ; i < Number(amount) ; i++) {
                let obj = {};
                obj['height'] = fixedHeight ? 50 : this.randomInteger(30, 90);
                obj['id'] = i;
                obj['color'] = '#' + this.randomColor();
                arr.push(obj);
            }
            return arr;
        },
        randomColor() {
            return Math.floor(Math.random() * 16777215).toString(16);
        },
        randomInteger(min, max) {
            return Math.floor(Math.random() * (max - min + 1) ) + min;
        }
    }
}
</script>
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值