一个懒加载和虚拟滚动的案例

1,599 阅读4分钟

实现一个虚拟滚动,配合懒加载,滚动到相应位置再加载数据。

从外部进入,自动跳转到对应的位置,支持上下个数据切换。

1.gif 从外部第三页跳转进来,自动定位到列表项的位置,可以滚动,自动加载相应区域的列表。

没加载过的列表会有小段时间白屏哈~

主要用这个: blog.csdn.net/qq_42268364…

其他: zhuanlan.zhihu.com/p/54327805 stackblitz.com/edit/react-…

github.com/gaogaolater…

blog.csdn.net/gaogao32/ar…

www.jianshu.com/p/e4ce9783d…

还有一些算单个数据的偏移量的,我觉得还是算容器的比较正常

一个相对定位div包着两个绝对定位的div

一个高度为全列表高度,负责撑开滚动条

一个负责渲染的区域,由于滚动,要有一定的偏移,transform

     <div
          className="slide-container"
          id="virtual"
          style={{ position: "relative" }}
        >
          <div
            style={{
              height: allHeight,
              position: "absolute",
              width: "100%",
              top: "0",
              left: "0",
            }}
            id="virtual2"
          />
          <div
            style={{
              transform: funcTran,
              position: "absolute",
              width: "100%",
              height: "100%",
            }}
          >
            <SlideList
              isShowList={isShowList}
              image_id={image_id}
              image_list={showList}
            />
          </div>
        </div>

我这边每个列表项高度为101px,类似 写了一堆数据,我写了防抖是为了不去重复请求未加载的数据

  • 列表总高度listHeight = listData.length * itemSize
  • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)
  • 偏移量startOffset = scrollTop - (scrollTop % itemSize);
 layoutContent.addEventListener(
      "scroll",
      debounce(async (e) => {
        // console.log("节流测试", e.timeStamp);
        let { height, page_size } = this.state;
        const { pageDataListMap, tableSource } = this.props;
        let top = e.target.scrollTop; 
        let itemSum = Math.ceil(height / 101); 
        let start = Math.floor(top / 101);
        let end = start + itemSum;
        let offsetTop = top - (top % 101);
        let funcTran = `translateY(${offsetTop}px)`;
        this.setState({ funcTran });

这里列表显示数据有点难整,因为我没有全列表数据,所以不能那么搞,我这边请求一次20条数据。

我将请求过的列表数据存于pageDataListMap对象中,key为页码,value为列表,需要计算start和end所在页码,再截取其中的列表项出来显示。如果页码数据还没加载就要去加载,是await去执行,所以没数据时屏幕会空白一下。有能力的可以搞骨架屏。。

  • 计算页码
//page_size:20,为啥+1,因为没0页。。
let page_index = Number(Math.floor(start / page_size) + 1);
let endPage = Number(Math.floor(end / page_size) + 1);
  • 拼接数据,start % page_size得到起始位置,拼数据,然后切割。+2是发现不加底部会留空我也不知道为啥,所以多搞点数据,展示列表就是showList
let startIndex = start % page_size;
showList = pageDataListMap.get(page_index).slice(startIndex);
if (endPage !== page_index) {
  showList = showList.concat(pageDataListMap.get(endPage));
}
showList = showList?.splice(0, itemSum + 2);

这就是核心了

定位跳

要知道跳转列表项的索引,本项目是新开页面加载的,所以把第三页和50条每页的参数存local,然后在这边重新请求这个请求,根据url上显示的列表项id,找到索引,index * 101就是定位的高度

tableSource 是存 local的数据参数,有页码和每页条数

// 前两页 100条
let total = (tableSource.page_index - 1) * tableSource.page_size;
const { rows: image_list } = await getRequestFun(tableSource);
      let index = 0;
      image_list?.forEach((item, i) => {
        if (item.id === image_id) {
          index = i;
        }
      });
    // 得出当前id在总列表中的序列
index += total;
let layoutContent = document.getElementById("virtual");
      // 滚动滚动条使图片出现在当前位置
      // 要判断是否滚动到最底不能再滚了,不能超到底
      // scrollTop最大等于scrollHeight-clientHeight
      let scrollTop = index * 101;
      if (scrollTop + layoutContent.clientHeight > layoutContent.scrollHeight) {
        layoutContent.scrollTop =
          layoutContent.scrollHeight - layoutContent.clientHeight - 50;
      } else {
        layoutContent.scrollTop = index * 101;
      }

跳转上下页按钮

点按钮不滚动列表,所以可能会有跳到未加载页码列表的情况,这边也要加载数据。

初始化和每次跳转后要判断有无上下个数据。若无要提前加载,实在没数据要禁用按钮。

每次切换列表条数时,需要遍历pageDataListMap,判断当前页数和索引,找出上一个和下一个的id,若没加载上一页和下一页,则请求列表加载

 componentDidUpdate(prevProps, prevState, snapshot) {
 。。。
     pageDataListMap.forEach(async (item, key) => {
      // 拿出列表数据
        let ids = item?.map((i) => i.id);
        // 如果包含当前列表项
        if (ids?.includes(image_id)) {
          // tableSource 是在local拿的数据
          if (tableSource.page_index !== key) {
                //更新local 新页码 必要的,因为不这样搞,有可能刷新后取不到定位不到位置
          }
          // 找出索引
          let index = ids.indexOf(image_id);
          // 如果是索引第一个 1
          if (index === 0) {
           // 而且是第一页,那没有上一页,上一页按钮禁用!
            if (key === 1) {
              this.setState({ preId: null, nextId: item[1]?.id || null });
            } else {
            // 不是第一页,拿出上一页,若每有请求过那就去请求。
            //。取出上一页最后一个数据当前一页,以此类推,后面不多说了
              let preArr = pageDataListMap.get(key - 1);
              if (!preArr) {
                let newTableSource = JSON.parse(JSON.stringify(tableSource));
                newTableSource.page_index = key - 1;
                newTableSource.page_size = page_size;
                const { rows: image_list } = await getRequestFun(
                  newTableSource
                );
                pageDataListMap.set(key - 1, image_list);
                this.props.fromListGetPageMap(pageDataListMap);
                preArr = image_list;
              }
              this.setState({
                preId: preArr[page_size - 1]?.id || null,
                nextId: item[1]?.id || null,
              });
            }
          } else if (index === page_size - 1) {
          // 如果是索引最后一位
            let nextArr = pageDataListMap.get(key + 1);
            if (!nextArr) {
              let newTableSource = JSON.parse(JSON.stringify(tableSource));
              newTableSource.page_index = key + 1;
              newTableSource.page_size = page_size;
              const { rows: image_list } = await getRequestFun(newTableSource);
              pageDataListMap.set(key + 1, image_list);
              this.props.fromListGetPageMap(pageDataListMap);
              nextArr = image_list;
            }
            // 没有下一页
            this.setState({
              preId: item[page_size - 2]?.id,
              nextId: nextArr[0]?.id || null,
            });
          } else {
            this.setState({
              preId: item[index - 1]?.id,
              nextId: item[index + 1]?.id,
            });
          }
        }
      });
 }

没有上一页id的话就禁用按钮

 <LeftOutlined
              className={!preId ? "disabled-icon" : "action-icon"}
              onClick={this.onPrevious}
            />

滚动快会重复请求

因为异步请求,同步滚的时候多次查当前位置页面数据没加载就去请求,异步没结束就多次查,就多次请求了

  • 做了防抖,虽然会有白屏,但基本不会有重复请求的情况
  • 请求时,将请求页码存变量数组里,每次请求去查数组里有没有这个页码再去请求
  • 重复请求时 取消请求,案例:mp.weixin.qq.com/s?__biz=MjM…

可优化

  • 白屏改骨架屏
  • 跳转上下页按钮里遍历查找页面,没必要那么麻烦,在local存的页码 上下页也找一下就行