虚拟滚动实现页面百万数据滚动

465 2023年09月11日 前端

1.概念

虚拟滚动是一种优化长列表性能的技术,它通过按需渲染的方式,只渲染可见部分的列表项,而不是渲染整个列表。

2.技术背景

在列表滚动中,如果列表数据有几万,几十万,就会生成大量的DOM元素,这样就会导致页面加载慢、卡顿甚至崩溃。因此,虚拟滚动技术,就是为了解决此问题而产生的。

3.原理

在具体实现上,虚拟滚动技术需要控制大列表中的DOM元素的创建与销毁,只创建可视区域内的DOM元素,非可视区域的DOM元素不创建。这样,在渲染大列表中的数据时,只创建少数的DOM元素,从而提高性能。虚拟滚动通过JS模拟的滚动来减少真实的滚动操作,防止页面加载慢、卡顿等问题,改善用户体验。

4.实现

以下是一个简单的示例,演示如何使用JavaScript和CSS实现虚拟滚动:

原生代码实现:

1)创建一个包含要显示的内容的HTML元素。

<div id="virtual-scroll">  
  <div class="item">Item 1</div>  
  <div class="item">Item 2</div>  
  <div class="item">Item 3</div>  
  <!-- 更多内容 -->  
</div>

2)使用CSS设置容器的高度和溢出属性,以便内容可以滚动。

#virtual-scroll {  
  height: 500px; /* 容器高度 */  
  overflow-y: auto; /* 垂直方向滚动 */  
}

3)使用JavaScript监听滚动事件,并动态创建和删除可见区域内的内容项。

const container = document.getElementById('virtual-scroll');  
const itemHeight = 50; // 每个内容项的高度  
const visibleItems = Math.floor(container.clientHeight / itemHeight); // 可见区域内的内容项数量  
  
function updateVisibleItems() {  
  const scrollTop = container.scrollTop;  
  const startIndex = Math.floor(scrollTop / itemHeight);  
  const endIndex = startIndex + visibleItems;  
  
  // 删除超出可见区域的内容项  
  const removeItems = container.getElementsByClassName('item');  
  for (let i = 0; i < removeItems.length; i++) {  
    if (i < startIndex || i >= endIndex) {  
      removeItems[i].remove();  
    }  
  }  
  
  // 创建新的内容项并添加到容器中  
  for (let i = startIndex; i < endIndex; i++) {  
    const item = document.createElement('div');  
    item.classList.add('item');  
    item.textContent = `Item ${i + 1}`;  
    container.appendChild(item);  
  }  
}  
  
// 监听滚动事件  
container.addEventListener('scroll', updateVisibleItems);

这个示例中,我们首先计算可见区域内的内容项数量,然后在滚动事件发生时动态创建和删除可见区域内的内容项。这种方法可以通过减少DOM元素的数量来提高性能,因为只需要渲染可见区域内的内容项。

vue中实现

1)创建一个虚拟滚动组件(VirtualScroll.vue):

<template>  
  <div class="virtual-scroll" ref="scrollContainer" @scroll="handleScroll">  
    <div class="phantom" :style="{ height: phantomHeight + 'px' }"></div>  
    <div class="visible-items" :style="{ transform: `translate3d(0, ${offsetTop}px, 0)` }">  
      <slot></slot>  
    </div>  
  </div>  
</template>  
<script>  
export default {  
  name: "VirtualScroll",  
  props: {  
    itemHeight: {  
      type: Number,  
      default: 20,  
    },  
    itemsCount: {  
      type: Number,  
      required: true,  
    }  
  },  
  data() {  
    return {  
      visibleItems: 0,  
      offsetTop: 0,  
      inThrottle:false, //截流参数
      timeout:false,     //防抖参数
    };  
  },  
  computed: {  
    phantomHeight() {  
      return this.itemsCount * this.itemHeight;  
    },  
  },  
  methods: {  
      handleScroll() {  
         if (!this.inThrottle) {  
            this.$nextTick(()=>{
            this.inThrottle = true;
                clearTimeout(this.timeout); 
                this.timeout = setTimeout(()=>{
                    const scrollTop = this.$refs.scrollContainer.scrollTop;
                    const startIndex = Math.floor(scrollTop / this.itemHeight);  
                    const endIndex = startIndex + this.visibleItems;  
                    this.offsetTop = scrollTop;
                                             
                    // 更新可见区域内的内容项  
                    this.$emit("update-items", startIndex, endIndex); 
                    this.inThrottle = false;  
                },300)
            })
            }
      },  
    },  
    mounted() {  
        this.$nextTick(()=>{
            this.visibleItems = Math.floor(this.$refs.scrollContainer.clientHeight / this.itemHeight);
            this.$refs.scrollContainer.addEventListener("scroll", this.handleScroll);  
        })
       
    },  
    beforeDestroy() {  
        this.$nextTick(()=>{
          this.$refs.scrollContainer.removeEventListener("scroll", this.handleScroll);  
        })
    },  
  };  
  </script>
  <style scoped>  
  .virtual-scroll {  
    position: relative;
    width: 100%;  
    height: 100%;  
    overflow-y: auto;  
  }  
  .phantom {  
    position: absolute;  
    top: 0;  
    left: 0;  
    right: 0;  
  }  
  .visible-items {  
    position: absolute;  
    height: 100%;
    top: 0;  
    left: 0;  
    right: 0;  
  }  
  </style>

2) 在父组件中使用虚拟滚动组件:

<template>
  <div id="List">
     <virtual-scroll  :item-height="50" :items-count="items.length" @update-items="updateItems">  
           <div v-for="index in showItems" :key="index" class="item">{{ index}}</div>  
    </virtual-scroll>  
  </div>
</template>


<script>
import VirtualScroll from "@/components/VirtualScroll.vue";
export default {  
    name: "App",  
    components: { VirtualScroll }, // 注册虚拟滚动组件  
    data() {  
        return { 
            items: [],
            showItems:[],
        }; 
    }, 
     mounted() {  
         this.$nextTick(()=>{
             this.getData();
         })
     },
    methods: { 
        async updateItems(startIndex, endIndex) { 
           
           this.showItems = await  this.items.slice(startIndex, endIndex)
            console.log(startIndex, endIndex)
     
        }, 
        async  getData(){
            for(let i=0; i<1000000; i++){
               this.items.push('item-' + i)
            }
            this.showItems = this.items.slice(0, 20)
            
     
        }
    }, 
}; 
 </script> 
 <style> 
 #List{
     height: 100vh;
 }
     .item { 
        height: 50px; /* 内容项的高度 */ 
        color: #2C3E50;
     } 
 </style>

uiapp中实现

1)子组件 VirtualScroll.vue

<template>  
  <view class="virtual-scroll" ref="scrollContainer" @scroll="handleScroll">  
    <view class="phantom" :style="{ height: phantomHeight + 'px' }"></view>  
    <view class="visible-items" :style="{ transform: `translate3d(0, ${offsetTop}px, 0)` }">  
      <slot></slot>  
    </view>  
  </view>  
</template>  
<script>  
export default {  
  name: "VirtualScroll",  
  props: {  
    itemHeight: {  
      type: Number,  
      default: 20,  
    },  
    itemsCount: {  
      type: Number,  
      required: true,  
    }  
  },  
  data() {  
    return {  
      visibleItems: 0,  
      offsetTop: 0,  
      inThrottle:false, //截流参数
      timeout:false,     //防抖参数
    };  
  },  
  computed: {  
    phantomHeight() {  
      return this.itemsCount * this.itemHeight;  
    },  
  },  
  methods: {  
      handleScroll() {  
        if (!this.inThrottle) {  
             this.$nextTick(()=>{
                this.inThrottle = true;
                clearTimeout(this.timeout); 
                this.timeout = setTimeout(()=>{
                    const scrollTop = this.$refs.scrollContainer.$el.scrollTop;
                    const startIndex = Math.floor(scrollTop / this.itemHeight);  
                    const endIndex = startIndex + this.visibleItems;  
                    this.offsetTop = scrollTop;
                                             
                    // 更新可见区域内的内容项  
                    this.$emit("update-items", startIndex, endIndex); 
                    this.inThrottle = false;  
                },300)
            })
        }
      },  
    },  
    mounted() {  
        this.$nextTick(()=>{
            if(this.$refs.scrollContainer){
                this.visibleItems = Math.floor(this.$refs.scrollContainer.$el.clientHeight / this.itemHeight);
                this.$refs.scrollContainer.$el.addEventListener("scroll", this.handleScroll); 
            }
             
        })
       
    },  
    beforeDestroy() {  
        this.$nextTick(()=>{
            if(this.$refs.scrollContainer){
                 this.$refs.scrollContainer.$el.removeEventListener("scroll", this.handleScroll);
            }
           
        })
    },  
  };  
  </script>
  <style scoped>  
  .virtual-scroll {  
    position: relative;
    width: 100%;  
    height: 100%;  
    overflow-y: auto;  
  }  
  .phantom {  
    position: absolute;  
    top: 0;  
    left: 0;  
    right: 0;  
  }  
  .visible-items {  
    position: absolute;  
    height: 100%;
    top: 0;  
    left: 0;  
    right: 0;  
  }  
  </style>

2)父组件 index.vue

<template>
    <view class="content">
        <view class="scroll-left">
            22
        </view>
        <view class="scroll-right" >
            <virtual-scroll  :item-height="50" :items-count="items.length" @update-items="updateItems">  
                       <view v-for="index in showItems" :key="index" class="item">{{ index}}</view>  
            </virtual-scroll>  
        </view>
    </view>
</template>

<script>  
import VirtualScroll from "@/components/VirtualScroll.vue"; // 导入虚拟滚动组件的路径可能有所不同,根据实际情况调整  
  
export default {  
  name: "App",  
  components: { VirtualScroll }, // 注册虚拟滚动组件  
  data() {  
    return {  
       items: [],
       showItems:[],
      
    };  
  },  
  mounted() {
    this.$nextTick(()=>{
      this.getData();
    })
  },
  methods: {  
    async updateItems(startIndex, endIndex) { 
               
       this.showItems = await  this.items.slice(startIndex, endIndex)
        console.log(startIndex, endIndex)
 
    }, 
    async  getData(){
        for(let i=0; i<1000000; i++){
           this.items.push('item-' + i)
        }
        this.showItems = this.items.slice(0, 20)
        
 
    }  
  },  
};  
</script>

<style scoped lang="less">
    
    
    .content{
        // 滚动条不可见
        
        display: flex;
        flex-direction: row;
        .scroll-left{
            &::-webkit-scrollbar {
                display: none;
                width: 0 !important;
                height: 0 !important;
                -webkit-appearance: none;
                background: transparent;
            }
            width: 200rpx;
            height: 90vh;
            overflow-y: auto;
            background-color: aliceblue;
        }
        .scroll-right{
            &::-webkit-scrollbar {
                display: none;
                width: 0 !important;
                height: 0 !important;
                -webkit-appearance: none;
                background: transparent;
            }
            flex: 1;
            height: 90vh;
            
            background-color: aqua;
            .item { 
                    height:50px; /* 内容项的高度 */ 
                 } 
        }
    }
</style>
评论

0 条评论
OBJUI公众号
热门文章
  • go语言怎么连接mysql,并实现增删改查

    要使用Go语言连接MySQL,需要使用第三方库。常用的库包括: 这里以go-sql-driver/mysql为 […]

  • 使用Echarts画甘特图

    Echarts是一个非常强大的图表库, 下面我们来使用它来画甘特图,

  • Beego实现JWT

    Beego是一个基于Go语言的Web框架,实现JWT认证可以通过beego的中间件机制来实现,下面是一个简单的 […]

  • beego实现模块化开发

    Beego 框架可以通过模块化开发来提高项目的可维护性和可扩展性,可以将一个大型的应用划分为多个模块,每个模块独立维护,有自己的控制器、视图和模型等。

  • go实现MD5

    在这个示例中,我们使用了Go标准库中的crypto/md5包来计算一个字符串的MD5值。首先,我们将字符串转换 […]

  • 使用Axios+PHP+JWT实现登录认证

    JWT(JSON Web Token),顾名思义就是可以在Web上传输的token,这种token是用JSON格式进行format的。它是一个开源标准(RFC7519),定义了一个紧凑的自包含的方式在不同实体之间安全的用JSON格式传输信息。

  • Linux服务器Rsync结合inotify同步文件

    一、实现效果 服务器A:192.168.161.150 (分布服务器)(rsync客户端+inotify) 服务器B:192.168.161.151 (WEB服务器1)(rsync服务端) 服务器C:192.168.161.152 (WEB服务器2)(rsync服务端) 说明:服务器A有文件更新,自动同步到服务器B和C

  • wordpress自定义配置

    在制作插件时,自定义的配置项是经常会遇到的。在左侧“配置”的常规菜单下添加配置以下代码可实现:

  • JavaScript中async和await的应用

    async和await是ES7中引入的关键字,用于简化Promise操作,提高Promise代码的可读性和理解性。它们结合使用可以使得异步调用不返回Promise对象,而直接把then回调函数的第一个形参result给返回出来,使代码更节俭,开发效率更高。

  • wordpress中使用事务处理

    修改Mysql引擎,因为InnoDB才支持事务