應用場景
前端的業務開發中會遇到不使用分頁方式來加載長列表的需求。如在數據長度大於 1000 條情況,DOM 元素的創建和渲染需要的時間成本很高,完整渲染列表所需要的時間不可接受,同時會存在滾動時卡頓問題;
解決該卡頓問題的重點在於如何降低長列表DOM渲染成本問題,文章將介紹通過虛擬列表渲染的方式解決該問題。
實現思路
虛擬列表的核心思想為可視區域渲染,在頁面滾動時對數據進行截取、復用DOM進行展示的渲染方式。
實現虛擬列表就是處理滾動條滾動后的可見區域的變更,其中具體步驟如下:
- 計算當前可見區域起始數據的 startIndex
- 計算當前可見區域結束數據的 endIndex
- 計算當前可見區域的數據,並渲染到頁面中
- 計算 startIndex 對應的數據在整個列表中的偏移位置 startOffset,並設置到列表上
基礎實現
我們首先要考慮的是虛擬列表的 HTML、CSS 如何實現:
- 列表元素(.list-view)使用相對定位
- 使用一個不可見元素(.list-view-phantom)撐起這個列表,讓列表的滾動條出現
- 列表的可見元素(.list-view-content)使用絕對定位,left、right、top 設置為 0
<template> <div class="list-view" :style="{ height: `${height}px` }" @scroll="handleScroll"> <div class="list-view-phantom" :style="{ height: contentHeight }"> </div> <ul ref="content" class="list-view-content"> <li class="list-view-item" :style="{ height: itemHeight + 'px' }" v-for="(item, index) in visibleData" :key="index"> {{ item }} </li> </ul> </div> </template> <script> export default { name: 'ListView', props: { data: { type: Array, default: function() { const list = [] for (let i = 0; i < 1000000; i++) { list.push('列表' + i) } return list } }, height: { type: Number, default: 400 }, itemHeight: { type: Number, default: 30 }, }, computed: { contentHeight() { return this.data.length * this.itemHeight + 'px'; } }, mounted() { this.updateVisibleData(); }, data() { return { visibleData: [] }; }, methods: { updateVisibleData(scrollTop) { scrollTop = scrollTop || 0; const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight); const start = Math.floor(scrollTop / this.itemHeight); const end = start + visibleCount; this.visibleData = this.data.slice(start, end); this.$refs.content.style.webkitTransform = `translate3d(0, ${ start * this.itemHeight }px, 0)`; }, updateVisibleData(scrollTop) { scrollTop = scrollTop || 0; const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight); // 取得可見區域的可見列表項數量 const start = Math.floor(scrollTop / this.itemHeight); // 取得可見區域的起始數據索引 const end = start + visibleCount; // 取得可見區域的結束數據索引 this.visibleData = this.data.slice(start, end); // 計算出可見區域對應的數據,讓 Vue.js 更新 this.$refs.content.style.webkitTransform = `translate3d(0, ${ start * this.itemHeight }px, 0)`; // 把可見區域的 top 設置為起始元素在整個列表中的位置(使用 transform 是為了更好的性能) }, handleScroll() { const scrollTop = this.$el.scrollTop; this.updateVisibleData(scrollTop); } } } </script> <style lang="scss" scoped> .list-view { overflow: auto; position: relative; border: 1px solid #aaa; width: 200px; } .list-view-phantom { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } .list-view-content { left: 0; right: 0; top: 0; position: absolute; } .list-view-item { padding: 5px; color: #666; line-height: 30px; box-sizing: border-box; } </style>