Taro高性能虚拟列表02

366 阅读3分钟

继上次优化之后效果产品还是不满意,所以又优化了一个版本,在iphone手机上基本做到了下拉秒加载,在红米9pro上测试效果也很好;不过有个上限,当上拉加载的商品超过200个的时候性能会有所下降;

Taro高性能虚拟列表

Taro高性能虚拟列表03

新的方案
  • 每页10个商品,上拉加载下一页商品
  • 每页10个商品是一组,每组是一个独立虚拟卡片,当该组商品不在屏幕中时,会被一张站位图替换
  • 相比之前的虚拟列表方案
    • 增加了分页,之前的是以商品维度的虚拟卡片,滑动的时候会频繁的渲染,并且商品卡片是不超过一屏幕的,空白站位图频繁出现

image.png

代码
/**
* 可分组的虚拟列表
*/
import { useEffect, useRef, useState } from 'react';
import { ScrollView, View } from '@tarojs/components';
import { AtActivityIndicator } from 'taro-ui';
import VirtualListItem from './virtualListItem';

interface VirtualScrollerListProps {
  /** 数据 */
  dataSource: any[];
  /** 分页每页多少 */
  pageSize: number;
  onScrollToUpper: () => Promise<void>;
  onScrollToLower: () => Promise<void>;
  [key: string]: any
}
/**
 * 将一维数组转换为二维数组
 *
 * @param arr 一维数组
 * @param rows 转换后的二维数组的行数
 * @returns 转换后的二维数组
 */
function convertTo2DArray(arr, pageSize) {
  var numRows = Math.ceil(arr.length / pageSize);
  var result: any = [];

  for (var i = 0; i < numRows; i++) {
    var startIdx = i * pageSize;
    var endIdx = startIdx + pageSize;
    var row = arr.slice(startIdx, endIdx);
    result.push(row);
  }
  console.log(result);
  return result;
}
export default function VirtualScrollerList({ dataSource, pageSize, onScrollToLower, onScrollToUpper, goodsType, orderType, activeClassifyType, ...lastProps }: VirtualScrollerListProps) {
  const [list, setList] = useState<any[]>([]);
  const scrollDirectionRef = useRef<'up' | 'down'>('down');
  const touchPosition = useRef<any>({}); // 上拉加载
  const [refresherTriggered, setRefresherTriggered] = useState(false); // 滚动列表的参数
  // const [scrollKey, setScrollKey] = useState('k' + String(Date.now()))
  const scrollKeyRef = useRef('k' + String(Date.now()))
  const loadingRef = useRef(false);
  const [paging, setPaging] = useState<{
    currnet: number,
    total: number
    pageSize: number
  }>({
    currnet: 1,
    total: 0,
    pageSize: 10
  })


  useEffect(() => {
    // 这个做二维分组
    const newList = convertTo2DArray(dataSource, pageSize);
    console.log(newList);

    Object.assign(paging, {
      currnet: 1,
      total: newList.length,
      pageSize: pageSize
    })
    scrollKeyRef.current = 'k' + String(Date.now());
    setList(newList)
  }, [dataSource])

  const onScrollToUpperFn = async () => {
    scrollDirectionRef.current = 'down';
    await onScrollToUpper()
    setRefresherTriggered(false); // 下拉刷新状态置为false
  };

  const onScrollToLowerFn = () => {
    console.log('onScrollToLower');
    if (loadingRef.current) return;
    loadingRef.current = true;
    setPaging({
      ...paging,
      currnet: paging.currnet + 1,
    })
    hideLoading();
  }

  const onTouchEnd = () => {
    console.log('onTouchEnd', touchPosition.current);
    if (loadingRef.current) return;

    const { start, end } = touchPosition.current;

    if (start && end && start.pageY - end.pageY >= 100 && paging.currnet >= paging.total) {
      loadingRef.current = true;
      console.log('next');
      onScrollToLower();
      hideLoading();
    }
  };

  const hideLoading = () => {
    setTimeout(() => {
      loadingRef.current = false;
    }, 500)
  }

  return (
    <>
      <ScrollView
        key={scrollKeyRef.current}
        scrollY
        scrollX={false}
        style={{ height: '100%' }}
        lowerThreshold={200}
        refresherThreshold={50}
        refresherEnabled
        refresherBackground='#F6F6F6'
        refresherTriggered={refresherTriggered}
        onScrollToLower={onScrollToLowerFn}
        refresherDefaultStyle='none'
        onRefresherPulling={() => {
          setRefresherTriggered(true);
        }}
        onRefresherRefresh={onScrollToUpperFn}
        onTouchStart={e => {
          console.log('onTouchStart');
          touchPosition.current = {
            start: e.touches[0]
          };
        }}
        onTouchMove={e => {
          console.log('onTouchMove')
          touchPosition.current = {
            ...touchPosition.current,
            end: e.touches[0]
          };
        }}
        onTouchEnd={onTouchEnd}
      >
        <View
          style={{
            position: 'absolute',
            width: '100%',
            height: '60px',
            background: '#F6F6F6',
            display: 'flex',
            alignItems: 'center',
            top: '-60px',
            textAlign: 'center',
            color: '#999999',
            justifyContent: 'center'
          }}>
          下拉加载更多
        </View>
        {
          (list.slice(0, paging.currnet)).map((subList: any, index: number) => {
            return (
              <VirtualListItem observeIdKey={`observe__${scrollKeyRef.current}_${index}`} key={index}>
                {/* 商品卡片 */}
                <CustomComponent></CustomComponent>
              </VirtualListItem>
            )
          })
        }
        <View key={paging.currnet} style={{ color: '#999999', textAlign: 'center', padding: '12rpx', display: 'flex', width: '100%', justifyContent: 'center', alignItems: 'center' }}>
          {paging.currnet >= paging.total ? '没有更多了' : <AtActivityIndicator content='加载中...'></AtActivityIndicator>}
        </View>
      </ScrollView>
    </>
  );
}
/*
 * 虚拟卡片
 */
import { CustomWrapper, View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import { useEffect, useRef, useState } from "react";
import { wxuuid } from "@/utils/helper";

interface VirtualListItemProps {
  children: React.ReactNode;
  observeIdKey?: string // 用于性能优化
}

export default function VirtualListItem({ children, observeIdKey }: VirtualListItemProps) {
  const observeId = observeIdKey || `observe_${wxuuid()}`
  const height = useRef(0);
  const [visible, setVisible] = useState(true);
  const timeoutRef = useRef<any>();

  useEffect(() => {
    let ob = Taro.createIntersectionObserver(this, {
      observeAll: true
    });
    timeoutRef.current = setTimeout(() => {
      ob.relativeToViewport().observe(`.${observeId}`, res => {
        height.current = res.boundingClientRect.height;
        const show = res.intersectionRatio !== 0;
        setVisible(show);
      });
    }, 10);

    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }
      ob.disconnect();
    };
  }, [observeId]);

  return (
    // CustomWrapper 放在最外层导致observe监听不到,所以放在最内层
    <View
      id={observeId}
      className={observeId}
      style={{
        width: "100%",
        height: height.current || '',
        background: visible ? "none" : "url(https://oss-cdn.mkh.cn/fe/image/2021-10-22/c178f22e-d5e5-4742-a0bf-cb8831ec505b.png) repeat-y",
        backgroundSize: "100% 400rpx"
      }}
    >
      {/* 这种写法有问题,包裹后更新visible无效 */}
      {/* <CustomWrapper>
        {visible && children}
      </CustomWrapper> */}
      {
        visible && (
          <CustomWrapper>
            {children}
          </CustomWrapper>
        )
      }
    </View>
  );
}
总结

可以看出这种方式和之前的方式比较,随着下拉数据的增多会多很多createIntersectionObserver监听对象,意味着消耗的性能更多,如果把pageSize设置为1,就是我尝试过最早的一个版本,会为每个商品卡片创建一个简体对象;因为产品是内部产品,视频放不了;