[Оптимизация данных проекта 3] Оптимизация данных длинного списка

Платформа цифрового управления
Vue3+Vite+VueRouter+Pinia+Axios+ElementPlus
Кейс системы разрешений Vue
адрес личного блога

Предисловие :

В мобильных проектах мы обычно используем ленивую загрузку для загрузки списка, преимущества очевидны, нет необходимости запрашивать данные за один раз, когда пользователь тянет вниз, используется ajax для динамического вытягивания следующих данных из сервер. Но это приводит к другой проблеме.Если пользователь сходит с ума, это заставит браузер создать несколько избыточных узлов, что приведет к избыточности, а сколько узлов у вас есть, сколько узлов Vue будет различаться.Такой сценарий принесет избыточные потребление производительности и использование памяти. Только представьте, если бы мы могли отрисовывать только узлы в зоне видимости пользователя, мы могли бы очень хорошо решить эту проблему — это фон виртуальной прокрутки.

Принцип виртуальной прокрутки:

Прежде всего, нам нужно знать, что виртуальная прокрутка реализована с помощью v-for Vue, Как объяснялось выше, виртуальная прокрутка отображает только видимую область, поэтому содержимое узла в нашей видимой области неизбежно изменится с полосой прокрутки пользователя. Предполагая, что страница может отображать только n узлов, как сделать так, чтобы n узлов, которые необходимо изменить, следовали за полосой прокрутки?

transform:translateY()Таким образом, используя CSS , нам нужно только позволить n узлам перемещаться с полосой прокрутки, где мы прокручиваем, и где узел заменяется.

Для реализации виртуальной прокрутки достаточно знать следующие условия:

  1. Сколько элементов может отображаться на странице?

    • Емкость страницы = размер страницы (clientHeight) / размер одного элемента

    • showNum = Math.floor(viewH / itemH) + 4 # Установите здесь еще несколько, чтобы предотвратить прямую замену при прокрутке, поэтому +4

  2. Какой узел должен начать рендеринг?

    Давайте предположим, что полоса прокрутки теперь прокручена до позиции x, можем ли мы посчитать, сколько узлов может разместиться на высоте x, а затем выяснить, какой узел должен начать рендеринг? Ответ — да, js предоставляет нам scrollTopэто свойство, чтобы получить высоту полосы прокрутки.

     getCurStart(scrollTop){
      // 卷去了多少个
      return Math.floor(scrollTop/(itemHeight));
    }
    
  3. Когда рендерить?

    Тайминг рендеринга тоже очень простой.Когда первый узел в списке задействован полностью, нам нужно выполнить рендеринг, т.к. в это время полностью задействованный узел уже не виден, нам нужно протолкнуть его вниз и потом рендерить это как данные следующего элемента может быть немного трудно понять, давайте поместим изображение~

    Как показано на рисунке выше, когда 1 откатывается (полностью выходит за пределы нашей видимой области), мы используем translateY в css, чтобы сдвинуть его вниз и преобразовать в 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 мс. Ниже приведена функция дросселирования оболочки.

    # 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 Options 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 Composition 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>

Supongo que te gusta

Origin blog.csdn.net/qq_39335404/article/details/129879952
Recomendado
Clasificación