最简单的长列表优化

·  阅读 525

一. 长列表性能问题

做移动端h5项目,经常会遇到的问题就是长列表展示,数据较少的时候还好,如果数据量过大时就会出现明显的性能问题,加载特别慢甚至卡死。

所以长列表性能优化是很常见的问题,我们项目组件库用的antd-mobile,所以首先想的是用antd-mobile的ListView,但是antd mobile的解决方案是先加载可视区域的数据,向下滑动时再加载下一部分数据,之前的渲染的元素不会清除。这样如果数据量特别大的话,页面上dom元素会很多,也会影响性能。

由于我们的每一行高度都是固定的,场景比较简单,所以决定自己写一个动态加载的列表。

二. 设计思路

虽然列表很长,但是大部分区域是看不到,所以只需要渲染可视区域的元素就可以了,用户在滑动的时候更新渲染的数据即可。

三. 代码设计

1.最简单实现

实现该方案,最重要的是确定两个参数,

  1. 开始数据的index,声明为startIndex
  2. 结束数据的index,声明为endIndex

观察上图就可以发现,startIndex就是背景真实列表的scrollTop除以每一行的高度,而endIndex就是startIndex加可视区域能显示的item数量,这个数量就是可视区域高度除以每一行的高度,也就是以下代码

每一个item行高rowHeight,这个是固定的,给定一个44px(我们项目中用的rem,只需要一个简单的计算即可),先写代码将页面展示出来。

//./index.jsx
import React from 'react';
import './index.less';

let rowHeight = 44;

class TestList extends React.Component {
    constructor(props) {
        super(props);
        let data = new Array(100000);
        for (let index = 0; index < data.length; index++) {
            data[index] = index;
        }
        this.allData = data;
        this.state = {
            dataSource: []
        };
        this.startIndex = 0;
    }

    componentDidMount() {
        const containerH = document.documentElement.clientHeight;
        this.size = Math.ceil(containerH / rowHeight) + 1;
        this.endIndex = this.startIndex + this.size;
        this.setState({
            containerH,
            dataSource: this.allData.slice(this.startIndex, this.endIndex)
        })
    }
  
  	scroll(e) {
    	//核心方法,下一步再写。
    }
  
    render() {
        const { dataSource, containerH } = this.state;
        const renderRow = (rowData, index) => {
            const top = this.startIndex * rowHeight + rowHeight * index;
            return (
                <div key={rowData}
                    className='item-row'
                    style={{ position: 'absolute', top: top }}>
                    <span>{rowData}</span>
                </div>
            );
        };
        return (
            <div
                style={{ height: containerH }}
                className='outer-cont'
              	onScroll={(e) => this.scroll(e)}
            >
                <div className="inner-cont" style={{ height: this.allData.length * rowHeight }}>
                    {dataSource.map((row, index) => renderRow(row, index))}
                </div>
            </div>
        );
    }
}

export default TestList;
复制代码
//./index.less
.outer-cont {
    position: relative;
    overflow: auto;
    .inner-cont {
        position: relative;
        .item-row {
            width: 100%;
            height: 44px;
            border-bottom: 1px solid;
        }
    }
}
复制代码

这个实现只展示了最开始的几条数据,滑动的时候数据还不能刷新,下面根据之前的分析添加onScroll方法。

scroll(e) {
  //如果数据量小于size,就不处理了
  if (this.allData.length <= this.size) return;
  const top = e.target.scrollTop;
  //获取scrollTop,计算startIndex和endIndex
  const topIndex = Math.floor(top / rowHeight);
  //如果滑动量太小,index没有改变则不动。
  if(this.startIndex === topIndex) return
  this.startIndex = topIndex;
  this.endIndex = this.startIndex + this.size; 
  //输出一下看看
  console.log(this.startIndex);
  //更新数据
  this.setState({
    dataSource: this.allData.slice(this.startIndex, this.endIndex)
  })
}
复制代码

上下滑动发现显示是没有问题的,但是每滑动一下都会触发数据刷新,刷新频率太高了,所以想着做一个缓冲去,一次多缓冲一些dom。

2. 缓冲区优化

基本思路如图,每次刷新数据的时候,多渲染一些dom作为缓冲,当快要滑动出缓冲区域的时候,再更新数据。

现在需要确定的细节就是,什么时候刷新数据。

  1. 一开始的时候,可视区域还是在安全的区间内,在这个范围内移动,不需要刷新数据。
  2. 当用户下滑,可视区域超出安全区域了,这个时候startIndex就需要向下移动。
  1. 相反,startIndex就需要向上移动。
  2. 其实2和3的情况不需要做区分,只需要在出了安全区域之后,移动startIndex,让topIndex落到安全区域即可,那就放在缓冲区域的最中间吧,这样算法就简单了, topIndex = startIndex + cacheSize/2。

好了,暂定cacheSzie = 20

import React from "react";
import "./index.less";

let rowHeight = 44;

class TestList extends React.Component {
  constructor(props) {
    //和之前代码一样
    ...
    this.cacheSize = 20;
  }

  componentDidMount() {
    //初始化的时候加缓冲区
    this.setState({
      containerH,
      dataSource: this.allData.slice(
        this.startIndex,
        this.endIndex + this.cacheSize
      ),
    });
  }

  scroll(e) {
    if (this.allData.length <= this.size) return;
    const top = e.target.scrollTop;
    let topIndex = Math.floor(top / rowHeight);
    //判断是否出了安全区域,这块的数据可以自己调整
    if (topIndex - this.startIndex < 3 || topIndex - this.startIndex > 17) {
      //按照上面的分析,重置startIndex
      let index = topIndex - cacheSize / 2 < 0 ? 0 : topIndex - this.cacheSize / 2;
      if (this.startIndex === index) return;
      this.startIndex = index;
      console.log(this.startIndex);
      this.endIndex = this.startIndex + this.size;
      this.setState({
        dataSource: this.allData.slice(
          this.startIndex,
          this.endIndex + this.cacheSize
        ),
      });
    }
  }

  render() {
    //代码和之前一样
    ...
    );
  }
}

export default TestList;
复制代码

再测试一下

刷新频率明显降低,每滑动8行刷新一次。

3.onScroll优化

做到这里没有太大的问题了,还有一个优化点时onScroll的问题,滑动的时候onScroll触发的次数非常多,这就导致需要做很多次计算,优化onScroll第一个想到的就是防抖,我写了一个简单的防抖函数,测试了一下,发现页面会出现短暂空白,用户体验很不好。在这个过程中,还发现react组件中用防抖的一个需要注意的点,异步方法中需要使用event的话,需要添加event.persist()方法。

所以放弃防抖函数的方法,在这里贴出来记录一下。

debounce(fn, delay = 100) {
    let timer = null;

    return function (event) {
      let context = this;
      event.persist && event.persist();
      if (timer) clearTimeout(timer);
      timer = setTimeout(function () {
        fn.call(context, event);
      }, delay);
    };
  }
复制代码

虽然防抖不行,但是还是有别的方法的,查了资料,看到有人说可以用IntersectionObserver,这个我还没有测试过,也不知道效果怎么样,后面测试完了再更新文章。

分类:
前端