vue长列表优化

写在前面:不知不觉 ~2020年的日子已经过去了3/4,看到微博热搜说:2020年还剩下3个月的时候,心情突然骤降~哈哈哈哈

切入正题:

什么是长列表优化?

我们为什么需要长列表优化?

我们怎样进行长列表优化

长列表优化

  在我们的日常工作中,会越到各种各样的列表,比如,我们通常采用分页的方式进行内容的逐渐获取,但是不可否认的是,当我们列表内容过多的时候,就会出现页面滑动卡顿、数据渲染较慢的问题,本次将以10000条数据为基准,进行长列表的优化说明

我们为什么要进行长列表优化

  进行长列表优化,其实和js在浏览器中执行的顺序有莫大的关系,在我们进行页面访问的时候,浏览器会根据页面进行渲染的进行,主要包含

  • Gui 渲染线程(页面渲染)
  • Js引擎线程(执行js脚本)
  • 事件触发线程(EventLoop轮询处理线程)
  • 事件(onClick)、定时器(setTimeout)、ajax(xhr)独立线程
  • Gui线程和js引擎线程是互斥的

我们的每一步操作,是否恰当或者是否阻塞浏览器的执行机制,都是有莫大的关联的

l浏览器中对于js的执行顺序

 

引擎线程和ui线程是共用一个线程,js的主线程是单线程的,但是其在执行过程中能够开发进行新的线程,

如何使用列表优化呢?

1.先进行10000的数据初渲染

<ul id="container"></ul> 
 <script>
   let total =100000;
   var con = document.getElementById("container")
   let timer = Date.now();
   //新版本浏览器,当存在循环的时候会统计
   for(let i =0;i<total;i++){
     let li = document.createElement("li");
     li.innerHTML = i;
     container.appendChild(li) 
   }
   setTimeout(()=>{
     //此时是渲染时间
    console.log(Date.now()-timer)//3884
   })  
 </script>

  我们在怼页面进行操作的时候,能够感觉到此时是卡顿的,白屏的时间也是达到了3秒多

2.切片,分段渲染页面

 <ul id="container"></ul> 
 <script>
   let total =100000;
   var con = document.getElementById("container")
    let timer = Date.now();
    let index = 0;
    let id = 0;//递增的内容
     //分片 根据数据大小划分,每次加载固定的数据
    function load(){
      index+=50;
      if(index<total){
        //定时器是一个宏任务,会等待ui渲染完成后执行
        //setTimeout 是一个宏任务,requestAnimationFrame也是一个额宏任务欧 
         requestAnimationFrame(()=>{ 
          for(let i=0;i<50;i++){
            //ie的浏览器需要实现文档碎片
            var fragment  = document.createDocumentFragment();//文档碎片
            let li = document.createElement("li");
            li.innerHTML=i
            fragment.appendChild(li)    
          } 
          load();
          con.appendChild(fragment)
       },0)
      }
    }
    //分片加载 会导致页面dom元素过多,造成页面的卡顿
    load();

  此时进行分片的处理方式,先让页面渲染50条数据,然后在逐渐渲染50条数据,达到了分片的目的,但是如果用户此时拉动比较快的话,页面数据还没有渲染出来,导致存在空白屏幕的情况,但是可以解决简单的分页数据获取的问题

vue的长列表优化

不管是切片还是分时进行加载,无非是解决dom节点过多的时候,给我们页面带来的压力问题,因此尝试动态加载固定数据的节点项目,进而让页面中的dom节点减少,渲染一定数据的节点数据;

可以通过按需进行加载dom,即在可视区域内根据当前滚动的高度和列表的长度进行渲染响应dom页面中减少dom的结构数据

 

 我们dom的节点只加载可视区域的内容

列表固定高度分析

思路分析

  1. 首先进行外侧的容器的scroll事件进行监听
  2. 计算当前滚动的高度
    1. 计算开始位置和结束位置
    2. 计算向上滚动的偏移量
  3. 进行可视区域数据计算

1.构造数据

import Mock from 'mockjs'
let  items  =[];
for(var i=0;i<500;i++){
    //使用mock.js进行生成数据
  items.push({id:i,value:Mock.Random.sentence(5,50)})
}

2. 使用列表进行数据传入

<virtalList :size=40 :remain=8 :items="items" :varlable=true>
      <!-- 获得scope的数据 -->
      <itemL slot-scope="{item}" :item="item"></itemL> 
 </virtalList>
virtalList.vue的dom结构
<!-- 能滚动的盒子 -->
  <div class="viewport" ref="viewport" @scroll="handlScroll">
    <!-- 滚动条 -->
    <div class="scroll-bar" ref="scrollbar"></div>
    <!-- 列表位置 -->
    <div class="scroll-list" :style="{transform:`translate3d(0,${offset}px,0)`}">
      <div v-for="item in visibleData" ref="items" :key="item.id" :vid="item.id">
        <!-- 通过插槽传出去 -->
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
样式结构

.viewport {
  overflow-y: scroll;
  position: relative;
}
.scroll-list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

3.参数说明

    size: Number, //当前每一项的高度
    remain: Number, //可见多少个
    items: Array, //当前项目

4.滚动监听后handlScroll的处理函数

由于我们每个列表的高度都是固定的,因此我们不需要计算高度,

  • 初始化视口和scrollBar的高度
  this.$refs.viewport.style.height = this.size * this.remain + "px"; //当前的一项高度*显示的项目高度
    this.$refs.scrollbar.style.height = this.items.length * this.size + "px"; //当前总长度*当前的项目高度
  • 页面滚动的时候
this.start = Math.floor(scrollTop / this.size); //列表开始的位置应该额等于滚动的位置/列表的高度
this.end = this.start + this.remain; //要渲染列表结束的位置等于列表开始的位置加上每一页数据展示的高度
//如果前面有预留渲染,应该把这个位置向上移动 
this.offset = this.start * this.size - this.prevCount * this.size;

5.计算可视化的数据列表,渲染该列表即可

computed: { 
    //可见数据有哪些
    visibleData() {
      let start = this.start - this.prevCount;
      let end = this.end + this.nextCount;
      return this.items.slice(start, end);
    },
  },

6.此时看到了prevCount 和nextCount ,是为了解决问题不一致的原因,比如上滑倒一半时候,可视化数据的前一个数据还没有渲染出来,此时导致数据是空白的,后面的数据也是一样,如果存在一般没有加载出来的时候,可能导致后面就是白色空白的页面,因此,此时需要将可视区域的前面几项和后面几项多加载出来,防止出现空白化的问题,定义prevCount和nextCount

computed: {
    //前面预留几个
    prevCount() {
      return Math.min(this.start, this.remain);
    },
    //后面预留几个
    nextCount() {
      return Math.min(this.remain, this.items.length - this.end);
    }, 
  },

这样就可以有效的防止滑动到可视区域的第一项的时候或者最后一项的时候为空而出现空白的页面

列表高度不固定的时候

但是在实际工作中,可能列表的高度是一个不固定的值,这个时候我们就需要进行动态的配置高度和显示,
思路分析
  1. 预估列表的初始高度值,和实际值的列表相差幅度不是很大
  2. 根据预估值存储每个列表的顶部top,高度、底部bottom
  3. 滚动时候根据二分查找方法查找到当前的初始区域,
  4. 更新列表中的实际高度

1.设置列表的 varlable 属性,表示当前的item的子项目是不固定高度的

 <virtalList :size=40 :remain=8 :items="items" :varlable=true>
      <!-- 获得scope的数据 -->
      <itemL slot-scope="{item}" :item="item"></itemL> 
    </virtalList>  

2.初始化列表的存储高度 预估高度

//缓存当前的高度,top志等信息,还有bottom
    cacheList() {
      this.positions = this.items.map((item, index) => {
        return {
          height: this.size,
          top: index * this.size,
          bottom: (index + 1) * this.size,
        };
      });

3.滚动时候根据scrollTop查找到当前可视区域开始显示位置和结束显示位置

 let scrollTop = this.$refs.viewport.scrollTop;
      if (this.varlable) {
        //要使用二分查找方法进行查找
        this.start = this.getStartIndex(scrollTop)
        this.end = this.start +this.remain;
        //设置偏移量
        this.offset = this.positions[this.start - this.prevCount] ?
        this.positions[this.start-this.prevCount].top:0
        console.log(this.offset)
        
      } else{
          ...固定高度的处理项目
      }

4.二分查找方法进行查找开始位置和结束位置

  getStartIndex(value){
       let start = 0; //开始
       let end  = this.positions.length -1;//结束位置
       let temp = null; //记录当前的高度临时值
       //当开始位置小于结束的位置的时候,就一直往下找 ----todo
       while(start <= end){
         //找到中金的位置
         let middleIndex = parseInt((start+end)/2);
         //中间位置bottom位置
         let middleValue = this.positions[middleIndex].bottom;
        //如果当前的middleValue与value相等,则可进行
         if(middleValue ==value){
            return middleIndex +1; 
         }else if(middleValue <value){//当前要查找的在右边 
            start = middleIndex+1 
         }else if(middleValue >value){ //当前要查找的在左边 
        //  temp为存储的临时数据 如果不存在middleValue == value的时候 返回这个临时的数据
           if(temp == null || temp > middleIndex){
             temp = middleIndex //找到范围
           }
           end = middleIndex -1; 
         } 
       }
        return temp;  
    },

二分查找法进行步骤分析

 

 

5.更新组件内存储的列表高度

此时列表是不固定高度的,因此在滚动的过程中,我们需要对列表的真实高度进行测试和赋值
在vue的updated的生命周期中进行更新数据操作

 updated() {
    //页面渲染完成后需要根据当前展示的数据,更新缓存区的内容
    this.$nextTick(()=>{
      //根据当前显示的,更新缓存中的height、bottom\top 最终更新滚动体哦的高度
      //获取当前可视化区域的item对应的值
      let nodes = this.$refs.items; 
      if(!(nodes && nodes.length>0)){
        return ;
      }
      //遍历当前所在节点信息,
      nodes.forEach(node=>{
        //获取当前items的真实高度
        let {height} = node.getBoundingClientRect();//真实高度 
        let id =  node.getAttribute('vid')-0; //获取当前的item的位置,id表示index
        let oldHeight = this.positions[id].height; //老的高度 
        let val = oldHeight - height; //高度差值 
        //如果当前元素存在高度差值,则当前高度需要进行增加/减少,这样,当前元素的后面元素的表示位置也需要在原高度上进行更改
        if(val){
          //当前节点的高度进行更新
          this.positions[id].height = height;
          this.positions[id].bottom = this.positions[id].bottom - val ;//底部增加了
          //将后续的所有人都向后移动
          for(let i = id+1;i<this.positions.length;i++){
            this.positions[i].top = this.positions[i-1].bottom; 
            this.positions[i].bottom = this.positions[i].bottom- val;
           } 
        } 
      })
      //动态的计算滚动条的高度  滚动条的高度就等于positions的最后一个元素的最底部的位置
      this.$refs.scrollbar.style.height = this.positions[this.positions.length-1].bottom +'px'
    })
  },
除了我们的高度,滚动条设置的为初始值,因此也需要进行更新

效果图展示:

 

 

 

留下一些疑问,我们在滚动的时候其实做着大量的计算操作,会不会影响性能呢?

这样做的好处又是什么呢?

 

 

  

 

 

posted @ 2020-09-15 20:32  MFYNGUFD  阅读(9854)  评论(1编辑  收藏  举报