【プロジェクトデータの最適化3】ロングリストデータの最適化

デジタル管理プラットフォーム
Vue3+Vite+VueRouter+Pinia+Axios+ElementPlus
Vue パーミッションシステム事例
個人ブログアドレス

序文:

モバイル プロジェクトでは、通常、遅延読み込みを使用してリストを読み込みます。利点は明らかです。一度にデータをリクエストする必要はありません。ユーザーが一番下までプルダウンすると、ajax を使用して次のデータが動的にプルされます。サーバー。しかし、これは別の問題を引き起こします。ユーザーが無茶苦茶にプルダウンすると、ブラウザが複数の冗長ノードを作成し、その結果冗長性が生じ、ノードの数と Vue のノード数の差分が発生します。そのようなシナリオでは冗長性がもたらされます。パフォーマンスの消費とメモリの使用量。想像してみてください、ユーザーの可視領域にノードのみをレンダリングできれば、この問題はうまく解決できます。これが仮想スクロールの背景です。

仮想スクロールの原理:

まず第一に、仮想スクロールは Vue の v-for を使用して実装されていることを知っておく必要があります。上で説明したように、仮想スクロールは表示領域のみをレンダリングするため、表示領域内のノードのコンテンツは必然的にユーザーのスクロール バーに応じて変化します。ページに n 個のノードしか表示できないと仮定した場合、変更する必要がある n 個のノードをスクロール バーに追従させるにはどうすればよいでしょうか?

このようにCSS を使用するとtransform:translateY()、スクロール バーで n 個のノードを移動させ、どこでスクロールし、どこでノードを置き換えるかだけが必要になります。

仮想スクロールを実装するには、次の条件だけを知っておく必要があります。

  1. 1 ページに表示できるアイテムの数は何ですか?

    • ページ容量 = ページ サイズ (clientHeight) / 単一アイテムのサイズ

    • showNum = Math.floor(viewH / itemH) + 4 # スクロール時に直接置換されないように、ここでさらにいくつか設定するため、+4

  2. どのノードでレンダリングを開始する必要がありますか?

    スクロール バーが x 位置までスクロールされていると仮定します。x の高さに収容できるノードの数を計算し、どのノードがレンダリングを開始するかを判断できますか? 答えは「はい」です。js はscrollTopスクロール バーの高さを取得するためにこのプロパティを提供します。

     getCurStart(scrollTop){
      // 卷去了多少个
      return Math.floor(scrollTop/(itemHeight));
    }
    
  3. いつレンダリングするか?

    レンダリングのタイミングも非常に簡単です。リストの最初のノードが完全に関与している場合、レンダリングを実行する必要があります。この時点では、完全に関与しているノードは表示されなくなっているため、それをプッシュダウンしてからレンダリングする必要があります。次の項目のデータは少しわかりにくいかもしれないので、写真を載せておきます〜

    上の図に示すように、1 がロールアウトされると (表示可能な領域から完全に外れる)、CSS の translationY を使用してそれを押し下げて 2 にレンダリングします。すると、表示可能な領域の外側に余分なノードがあることがわかります。スライディングの継続性を確保するために、複数の冗長ノードをセットアップできます。

  4. レンダリング方法

    この部分がコアコードです。ここで問題が発生します。js は毎回高頻度でトリガーされるコールバックに応答しないため、itemHeight で割り切れるオフセットを取得しないと、プルバックしたときに最初の項目が表示される可能性が非常に高くなります。ノードのオフセットが 0 ではありません。

    onScroll(){
      //scrollTop常量记录当前滚动的高度
      const scrollTop=this.$refs.list.scrollTop;
    
      const start=this.getCurStart(scrollTop);
      //对比上一次的开始节点 比较是否发生变化,发生变化后便重新渲染列表
      if(this.start!=start){
        //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量
        const offsetY = scrollTop - (scrollTop % this.itemHeight);
        //使用slice拿到需要渲染的那一部分
        this.renderList=this.list.slice(start,this.start+this.showNum);
        //这里的top用于设置translateY  transform:translateY(${top}px)
        this.top=offsetY;
      }
      this.start=start;
    }
    
  5. 最適化

    onScroll は高頻度のトリガー コールバックであり、パフォーマンスの消費を抑えるために、少なくとも 50 ミリ秒で 1 回トリガーされるように制限する必要があります。以下はラッパー スロットリング関数です。

    # throttle.js 
    export default function(fn, delay) {
        let lock = false;
        return (...args) => {
            if (lock)
                return;
            //进入加锁
            lock = true;
            setTimeout(() => {
                fn.apply(this, args);
                //执行完毕解锁
                lock = false;
            }, delay);
        }
    }
    

ページ内をスクロールするための完全なコード (Vue2 オプション API):

<template>
    <div class="list" @scroll="scrollHandle" ref="list">
        <div class="item" v-for="(item,index) in renderList" :key="index"  :style="`height:${itemHeight}px;line-height:${itemHeight}px;transform:translateY(${top}px)`">
          {
   
   {item}}
        </div>
    </div>
</template>
<script>
import throttle from '@/utils/throttle';
export default {
  name: 'App',
  data() {
    return {
      list:[],//完整列表
      itemHeight:60,//每一项的高度
      renderList:[],//需要渲染的列表
      start:0,//开始渲染的位置
      showNum:0,//页面的容积:能装下多少个节点
      top:0,
      scroll,//用于初始化节流
    }
  },
  mounted() {
    this.initList();
    const cHeight=document.documentElement.clientHeight
    //计算页面能容纳下几个节点并且设置四个节点作为冗余
    this.showNum=Math.floor(cHeight/this.itemHeight)+4;
    //设置要渲染的列表 设置成能够容纳下的最大元素个数
    this.renderList=this.list.slice(0,this.showNum);
    //初始化节流函数 最短50毫秒触发一次
    this.scroll=throttle(this.onScroll,50);
  },
  methods: {
    //初始化列表 ,循环渲染 500条
    initList(){
      for(let i=0;i<500;i++){
        this.list.push(i);
      }
    },
    scrollHandle(){
      this.scroll();
    },
    onScroll(){
      //scrollTop常量记录当前滚动的高度
      const scrollTop=this.$refs.list.scrollTop;

      const start=this.getCurStart(scrollTop);
      //对比上一次的开始节点 比较是否发生变化,发生变化后便重新渲染列表
      if(this.start!=start){
        //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量
        const offsetY = scrollTop - (scrollTop % this.itemHeight);
        //使用slice拿到需要渲染的那一部分
        this.renderList=this.list.slice(start,this.start+this.showNum);
        //这里的top用于设置translateY  transform:translateY(${top}px)
        this.top=offsetY;
      }
      this.start=start;
    },
    getCurStart(scrollTop){
      //卷去了多少个
      return Math.floor(scrollTop/(this.itemHeight));
    }
  },
}
</script>

<style>
    *{
      margin: 0;
      padding: 0;
    }
    .list{
      height: 100vh;
      overflow: scroll;
    }
    .item{
      text-align: center;
      width: 100%;
      box-sizing: border-box;
      border-bottom: 1px solid lightgray;
    }
</style>

コンテナ内のスクロールコード (Vue3 コンポジション API)

<script setup>
    /**
     * 项目列表数据越来越多(上万条),正常列表可以分页,但是像下拉框之类组件就不能分页。每次都要加载所有的(很慢),性能不好的浏览器特别卡顿。虚拟滚动的技术完美解决。
     * 主要用于无法使用分页功能的长列表首屏加载速度慢问题,DOM加载过多“无用”元素。
     * 核心:
     *      1. 元素监听scroll事件
     *      2. 计算可视化高度一次能装几个列表,然后从总数据中进行slice截取
     *      3. 每一次滚动后根据scrollTop值获取一个可以整除itemH结果进行偏移
     */
    import { onMounted, reactive, ref } from 'vue';
    const listEle = ref()
    // 上万条总数居
    // const list = reactive(Array.apply(null, { length: 100000 }).map((v, i) => i))
    const list = reactive(Array.from({ length: 100000 }).map((_, i) => {
        return {
            key: i,
            value: i + 1
        }
    }))

    // 页面高度
    const viewH = 800
    // 单项高度
    let itemH = 200
    // 整个滚动列表高度
    let scrollH = itemH * list.length
    // 可视化高度一次可装列表数量(多设置几个防止滚动时候直接替换)
    let showNum = Math.floor(viewH / itemH) + 4
    // 页面上展示的数据
    let showList = reactive(list.slice(0, showNum))
    // 动态偏移量
    let offsetY = ref(0)
    // 时间戳
    let latestTime = new Date().getTime()

    onMounted(() => {
    })

    let timer = ref(null)
    const handleScroll = (e) => {
        if (new Date().getTime() - latestTime > 10) {
            clearInterval(timer.value)
            timer.value = setTimeout(() => {
                // 获取卷去的高度
                let scrollTop = e.target.scrollTop
                // 每一次滚动后,根据卷去高度 scrollTop 值,获取一个可以整除单项高度 itemH 的结果进行偏移
                // 例如: 卷去的 scrllTop = 1020  1020 % itemH = 20  offsetY = 1000
                offsetY.value = scrollTop - (scrollTop % itemH)
                // 更新数据:被卷去几条数据,就要往下增加几条数据
                showList = list.slice(Math.floor(offsetY.value / itemH), Math.floor(offsetY.value / itemH) + showNum)
                // 更新时间戳
                latestTime = new Date().getTime()
                console.log(showList)
            }, 300);
        }
    }
</script>
<template>
    <div :style="`height:${viewH}px;overflow-y:scroll;`" @scroll="handleScroll">
        <ul ref="listEle">
            <li v-for="item in showList" :key="item.key"
                :style="{ 'transform': `translateY(${offsetY}px)`, 'height': `${itemH}px`, 'line-height': `${itemH}px` }">{
   
   {
                    item.value }}</li>
        </ul>
    </div>
</template>
<style scoped>
    li {
        /* height: 200px;
        line-height: 200px; */
        color: #fff;
        font-size: 50px;
        font-weight: bold;
        text-align: center;
        background-color: orange;
    }

    li:nth-of-type(odd) {
        background-color: blue;
    }
</style>

おすすめ

転載: blog.csdn.net/qq_39335404/article/details/129879952
おすすめ