React+TS实现长列表性能优化实例(虚拟列表)

实现思路
将数据存储为trueData([[data], [data], [data]])和virtualTrueData([[data], [], []])两份,trueData存储所有数据,virtualTrueData内仅和body相交的分页有值,其他分页为空数组,使用高度撑开。

使用 IntersectionObserver 监听每一个分页是否和body相交,相交则将trueData中对应分页值写入virtualTrueData,反之则置空virtualTrueData对应分页值。

在页面加载完成及有新的分页加载完成时获取元素高度并给添加style给父节点撑开高度。

添加触底容器,监听触底事件并调用,获取返回值后入栈trueData及virtualTrueData。

代码展示
1.
组件源码

interface IProps<T> {
  data: T[];
  key?: string;
  maxPages: number;
  itemRender: (item: T, index: number, childIndex: number) => JSX.Element;
  reachBottom: () => any;
}
type loadProps = 'more' | 'loading' | 'noMore';
 
function OptimizedReactiveHeight<T>({
  data,
  key = 'id',
  itemRender,
  maxPages = 0,
  reachBottom = () => {},
}: IProps<T>) {
  const isCanCreat = useRef<boolean>(true);
  const newPageCreat = useRef<boolean>(false);
  const contentObserver = useRef<IntersectionObserver | null>(null);
  const footerObserver = useRef<IntersectionObserver | null>(null);
  const trueData = useRef<Array<Array<unknown>>>([data]);
  const virtualTrueRef = useRef<Array<Array<any>>>([data]);
  const statusRef = useRef<loadProps>('more');
 
  const [virtualTrueData, setVirtualTrueData] = useState<Array<Array<any>>>([data]);
  const [listStatus, setListStatus] = useState<loadProps>('more');
   
  useLayoutEffect(() => {
      if (isCanCreat.current) {
        // 创建分页监听
        isCanCreat.current = false;
        contentObserver.current = new IntersectionObserver(entries => {
          if (newPageCreat.current) {
            newPageCreat.current = false;
            return;
          }
          console.log('content entries', entries);
          entries.forEach(item => {
            if (item.intersectionRatio <= 0) {
              virtualTrueRef.current[item.target.id] = [];
              setVirtualTrueData([...virtualTrueRef.current]);
            } else {
              virtualTrueRef.current[item.target.id] = trueData.current[item.target.id];
              setVirtualTrueData([...virtualTrueRef.current]);
            }
          });
        }, {});
        // 脱离更新机制给节点添加高度
        setTimeout(() => {
          const listEl: any = document.getElementsByClassName('visible-list');
          for (let i = 0; i < listEl.length; i++) {
            const height = listEl?.[i].offsetHeight || 200;
            listEl[i].style.height = `${height}px`;
            // @ts-ignore
            contentObserver.current?.observe(listEl?.[i]);
          }
        }, 0);
        // 添加触底监听
        const footEl = document.getElementsByClassName('list-footer');
        footerObserver.current = new IntersectionObserver(entries => {
          console.log('footer entries', entries);
          // 加载下一页
          if (entries[0].intersectionRatio > 0) {
            onReachBottom();
          }
        }, {});
        // @ts-ignore
        footerObserver.current.observe(footEl[0]);
      }
     
    return () => {
      // @ts-ignore
      contentObserver.current.disconnect();
    };
  }, []);
 
  const onReachBottom = useCallback(async () => {
    if (statusRef.current === 'loading') {
      return false;
    }
    if (statusRef.current === 'noMore') {
      return false;
    }
    setListStatus('loading');
    statusRef.current = 'loading';
    let res = await reachBottom();
    if (res && res.length) {
      newPageCreat.current = true;
      trueData.current.push(res);
      virtualTrueRef.current.push(res);
      setVirtualTrueData([...virtualTrueRef.current]);
      setTimeout(() => {
        const listEl: any = document.getElementsByClassName('visible-list');
        const height = listEl[listEl.length - 1].offsetHeight || 2000;
        listEl[listEl.length - 1].style.height = `${height}px`;
        // @ts-ignore
        contentObserver.current.observe(listEl[listEl.length - 1]);
      }, 0);
    } else {
      setListStatus('noMore');
      statusRef.current = 'noMore';
    }
  }, []);
 
 
  useEffect(() => {
    if (virtualTrueData.length >= maxPages) {
      setListStatus('noMore');
      statusRef.current = 'noMore';
    } else {
      setListStatus('more');
      statusRef.current = 'more';
    }
  }, [virtualTrueData, maxPages]);
 
  return (
    <div className={styles.container}>
      <div className="visible-list-box">
        {virtualTrueData?.map((item, index) => (
          <div key={index} id={String(index)} className="visible-list">
            {item &&
            item?.map((childItem, childIndex) => (
              <div key={childItem[key] || childIndex}>
                {itemRender(childItem, index, childIndex)}
              </div>
            ))}
          </div>
        ))}
        <div className="list-footer" style={{ minHeight: '100px' }}>
          <LoadMore status={listStatus} />
        </div>
      </div>
    </div>
  );
}
  1. loadMore组件参考
const components = {  more: <div className={styles.more}>上拉加载</div>,
 loading: (
   <div className={styles.loading}>
     <img
       src=""
       style={{ width: '44px', height: 'auto' }}
       alt="加载中"
     />
   </div>
 ),
 noMore: <div className={styles.noMore}>已经到底了~</div>,
};

const LoadMore = ({ status = 'more' }: { status: 'more' | 'loading' | 'noMore' }) => {
 return <div className={styles.noSchool}>{components[status]}</div>;
};
  1. 样式
 commonStyle() {
  font-size: 14px;
  font-weight: 400;
  color: #AEB1BD;
  line-height: 14px;
  height: 14px;
  text-align: center;
  padding: 40px 0
}
 
@keyframes turn{
  0%{-webkit-transform:rotate(0deg);}
  25%{-webkit-transform:rotate(90deg);}
  50%{-webkit-transform:rotate(180deg);}
  75%{-webkit-transform:rotate(270deg);}
  100%{-webkit-transform:rotate(360deg);}
}
.more {
  commonStyle();
}
.loading {
  commonStyle();
  :global(.at-icon-loading) {
    animation:turn 1s linear infinite;
  }
}
.noMore {
  commonStyle()
  display: flex;
  align-items: center;
  justify-content: center;
}

4、使用方式

const listView = useCallback((item, index, childIndex) => {
    return (
      <div
        className={styles.homeList}
        onClick={() => {
          toDetail(item, index, childIndex);
        }}
      >
        {index}
      </div>
    );
  }, []);
  // 页面触底事件
  const reachBottom = useCallback(async () => {
    let res = await getList().catch(err => {
      console.log('reachBottom err---->', err);
      return [];
    });
    return res;
  }, []);
 // data及reachBottom触底事件均返回list的对象数组即可
 <HappyList
  data={data}
  maxPages={pagesNum}
  itemRender={listView}
  reachBottom={reachBottom}
 />

目前存在的问题
maxPage不支持动态传入(解决中)

删除/刷新节点未实现(解决中)

兼容性问题
在这里插入图片描述


作者:zzppff
git链接待更新
原创方法,商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React TS是一个用于构建用户界面的JavaScript库,而VR看房可以通过使用WebVR技术来实现。在React TS实现VR看房需要以下步骤: 1. 了解WebVR技术:WebVR是一种使Web应用程序能够在虚拟现实设备上运行的技术,它提供了一种在虚拟现实设备上渲染内容的方式。 2. 导入WebVR库:在React TS项目中,需要导入与WebVR相关的库,例如A-Frame或React 360等,这些库提供了一些用于构建VR界面的组件和功能。 3. 创建VR场景:使用React TS的组件化开发方式,可以通过在项目中创建VR场景组件来构建VR看房的界面。可以使用库中提供的组件来构建3D场景、添加虚拟现实设备的交互等。 4. 加载房屋模型:在VR场景中加载房屋模型,可以使用库中提供的加载器将3D房屋模型导入到场景中,并设置适当的位置和缩放。 5. 添加交互功能:为了实现VR看房的功能,需要添加一些交互功能,例如移动、旋转和缩放房屋模型,点击房间以获取更多信息等。可以使用库中提供的交互组件或自定义事件处理程序来实现这些功能。 6. 兼容不同的设备:考虑到不同的虚拟现实设备,需要在React TS项目中进行一些适配工作,以确保VR看房界面在不同设备上的兼容性。 总的来说,使用React TS实现VR看房需要对WebVR技术有一定的了解,并结合具体的库和组件来构建VR场景、加载房屋模型,并添加交互功能。不同设备的兼容性也是一个需要考虑的因素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值