十六、前端如何渲染几十万条数据不卡帧

337 阅读1分钟

第一、合理使用文档碎片和延迟加载渲染,逐帧绘制十几万条数据

比如有10万条数据,可以将这些数据先进行分片,每20个一帧。

在绘制帧内容时,可以使用 document.createDocumentFragment() 方法将20条数据先逐个放置到一个文档碎片中,然后将整个文档碎片渲染到页面上。

在开始一帧帧绘制时,可以使用 window.requestAnimationFrame() 进行有节奏的延迟绘制,等全部绘制完毕后,再使用 window.cancelAnimationFrame() 停止绘制。

<body>
    <ul></ul>
    <script>
        const ulEl = document.getElementsByTagName('ul')[0]
        const total = 100000
        const piece = 20
        const count = Math.ceil(total/piece)

        let currentCount = 0
        function add () {
            const docFragment = document.createDocumentFragment()
            for(let i = 0; i < piece; i++) {
                const liNode = document.createElement('li')
                liNode.innerText = Math.floor(Math.random() * 100000)
                docFragment.appendChild(liNode)
            }
            ulEl.appendChild(docFragment)
            currentCount++
            loop()
        }
        function loop() {
            if (currentCount < count) {
                // 延迟加载
                let d = window.requestAnimationFrame(add)
                console.log(d)
            } else {
                window.cancelAnimationFrame(d)
            }
        }
        loop()
    </script>
</body>

补充:

定时器是一个宏任务,会等待ui渲染完后执行下一个宏任务(定时器)

image.png

但是上面的分片加载也有缺陷:

导致页面元素过多,造成卡顿,所以可以使用虚拟列表优化

第二、虚拟列表优化

因为只展示可视区域的数据,所以

  • 首先需要知道每一项的高度,及希望展示的数据条数,获取可视区域的高度。
  • 还要获取数据的总条数,用来算出完整页面的高度,生成一个滚动条
  • 在可视区域中,通过截取的方式展示部分需要展示的数据
  • 监听滚动事件,如果滚动出2个,则从第三个开始重新渲染
  • 但滑动过快时,会出现头部尾部空白的情况,所以设置预加载,比如同时加载前后三屏

当前只实现 每一行定高的情况

引用页面

<template>
   <VirtualList :size="40" :remain="8" :initData="initData">
       <!-- 因为是截取渲染,所以v-for在virtual-list组件中进行 -->
       <VirtualItem slot-scope="{item}" :info="item"></VirtualItem>
   </VirtualList>
</template>

<script>
import VirtualList from './virtual/VirtualList'
import VirtualItem from './VirualItem.vue'
const initData = new Array(1000).fill().map((item, i) => ({
    value: i
}))
export default {
    data() {
        return {
            initData
        }
    }, 
    components: {
        VirtualList,
        VirtualItem
    }
}
</script>

自定义item页面

<template>
    <h1>{{info.value}}</h1>
</template>

<script>
export default {
    props: {
        info: Object
    },
}
</script>

组件页面

<template>
    <div class="container"
        ref="container"
        @scroll="handleScroll">
        <div class="all-page"
            ref="allPage"></div>
        <ul class="viewport" :style="{transform: `translate3d(0,${offset}px,0)`}">
            <li v-for="(item, index) in visibleData"
                :key="index"
                style="height: 40px; text-align: center; font-size: 26px;border-bottom: 1px solid red; box-sizing: border-box;">
                <!-- 定义插槽是为了让使用改组件的用户自定义要展示的内容 -->
                <slot :item="item"></slot>
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    props: {
        size: Number,
        remain: Number,
        initData: Array
    },
    data() {
        return {
            start: 0,
        }
    },
    computed: {
        visibleData() {
            const start = this.start - this.prevCount
            const end = this.end + this.nextCount
            return this.initData.slice(start, end)
        },
        end() {
            return this.start + this.remain
        },
        offset() { // 定义当前可视区域
            // return this.start * this.size 
            return this.start * this.size - this.size * this.prevCount
        },
        prevCount() { // 前面预留几个
            return Math.min(this.start, this.remain)
        },
        nextCount() { // 后面预留几个
            return Math.min(this.remain, this.initData.length - this.end)
        }
        
    },
    methods: {
        handleScroll() {
            const scrollTop = this.$refs.container.scrollTop
            this.start = scrollTop / this.size // 130/ 40 3.25( 从第四个开始渲染)

        }
    },
    mounted() {
        this.$refs.container.style.height = this.size * this.remain + 'px'
        this.$refs.allPage.style.height = this.size * this.initData.length + 'px'
    }

}
</script>

<style lang="less">
.container {
    overflow-y: auto;
    position: relative;
    background: pink;
}
.viewport {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    background: powderblue;
}
.all-page {
    background: yellowgreen;
}
</style>