当前位置:首页 >> 跨学科知识体系 >> 【react.js + hooks】useVirtualArea 渲染虚拟列表,htc velocity

【react.js + hooks】useVirtualArea 渲染虚拟列表,htc velocity

cpugpu芯片开发光刻机 跨学科知识体系 12
文件名:【react.js + hooks】useVirtualArea 渲染虚拟列表,htc velocity 【react.js + hooks】useVirtualArea 渲染虚拟列表 useVirtualArea Hook

useVirtualArea 是一个 React Hook,用于创建虚拟列表。虚拟列表是一种优化技术,用于在不影响性能的情况下显示大量数据。

参数

useVirtualArea 接受一个对象和一个数组作为参数,该对象包含以下属性:

loadMoreItems: 一个函数,当需要加载更多数据时会被调用。items: 当前的列表项。hasMore: 一个布尔值,表示是否还有更多的数据可以加载。height: 容器的高度。style: 容器的样式。containerComponent: 用于包裹列表的容器(默认div)。containerComponentProps: 传递给 containerComponent 的 props。renderTop: 用于渲染列表顶部的元素。renderItem: 用于渲染列表项的函数。itemComponent: 用于包裹列表项的容器(默认div)。itemComponentProps: 传递给 itemComponent 的 props。renderNoData: 没有列表数据时渲染的元素renderLoader: 用于渲染加载器的容器(默认div)。renderUnLoaded: 用于渲染没有更多数据时的元素。loaderComponent: 用于包裹加载器的组件。loaderComponentProps: 传递给 loaderComponent 的 props。renderBottom: 用于渲染列表底部的元素。observerOptions: 传递给 IntersectionObserver 的选项。

数组:依赖项

返回值

useVirtualArea 返回一个数组,包含以下元素:

loaderRef: 一个 ref,指向加载器的 DOM 元素。loading: 一个布尔值,表示是否正在加载数据。items: 当前的列表项。render: 一个函数,用于渲染列表。 实现 useVirtualArea Hook 步骤 1:定义 Hook 和参数

首先,我们需要定义我们的 Hook 和它的参数。我们的 Hook 将接受一个对象作为参数,该对象包含我们需要的所有配置选项。

import { useState, useRef } from 'react';interface VirtualAreaOptions {loadMoreItems: () => Promise<void>;items: any[];hasMore: boolean;// ...其他参数}export function useVirtualArea({ loadMoreItems, items, hasMore, ...rest }: VirtualAreaOptions, depths?: any[]) {// ...} 步骤 2:定义状态和 refs

然后,我们需要定义我们的状态和 refs。我们需要一个状态来跟踪是否正在加载数据,以及一个 ref 来引用加载器的 DOM 元素。

const [loading, setLoading] = useState(false);const loaderRef = useRef<any>(null); 步骤 3:使用 IntersectionObserver

接下来,我们需要创建一个 IntersectionObserver 来检测当加载器进入视口时。当这发生时,我们将调用 loadMoreItems 函数加载更多数据。

IntersectionObserver 是一个浏览器 API,用于异步观察目标元素与其祖先元素或顶级文档视口的交叉状态。这个 API 非常有用,因为它可以让你知道一个元素何时进入或离开视口,而无需进行复杂的计算或监听滚动事件,当被监听的元素进入视口,触发回调事件。 详见 MDN文档 - IntersectionObserver

useEffect(() => {const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting && hasMore && !loading) {setLoading(true);loadMoreItems().then(() => {setLoading(false);});}},{ ...observerOptions });if (loaderRef.current) {observer.observe(loaderRef.current);}return () => {observer.disconnect();};}, [loadMoreItems, hasMore, loading, observerOptions]); 步骤 4:返回值

最后,我们的 Hook 需要返回一些值。我们将返回一个数组,包含加载器的 ref、加载状态、列表项以及一个渲染函数。

return [loaderRef, loading, items, render];

在这个 render 函数中,我们将渲染所有的列表项和加载器。当 loading 为 true 时,我们将显示加载器,当 loading 为 false 并且 hasMore 为 false 时,我们将显示一个表示没有更多数据的元素;当列表没有时,将展示对应的 noData 元素。

render :

const render = useCallback(() => {return (<Container {..._containerComponentProps}>{typeof renderTop === "function" ? renderTop() : renderTop}{/** @ts-ignore */(items || []).length === 0 &&(typeof renderNoData === "function"? renderNoData(): renderNoData === void 0? "No data": renderNoData)}{items.map((item, index) => (<Item key={index} {...itemComponentProps}>{typeof renderItem === "function" ? renderItem(item) : renderItem}</Item>))}{/** @ts-ignore */}<Loader ref={loaderRef} {...loaderComponentProps}>{loading &&(typeof renderLoader === "function"? renderLoader(): renderLoader === void 0? "Loading...": renderLoader)}{!loading &&!hasMore &&(typeof renderUnLoaded === "function"? renderUnLoaded(): renderUnLoaded === void 0? "No more data": renderUnLoaded)}</Loader>{typeof renderBottom === "function" ? renderBottom() : renderBottom}</Container>);}, [_containerComponentProps,renderTop,items,Item,itemComponentProps,renderItem,loaderRef,loaderComponentProps,loading,renderLoader,hasMore,renderUnLoaded,renderBottom,...(depths || []),]); 步骤5 性能优化

尽可能的使用 useMemo 和 useCallback 来提升虚拟列表的性能。

最终效果图:

示例代码(css代码是全局注册了@emotion, Loading是自己封装的组件):

import { useState } from "react";import { useVirtualArea } from "@hooks/useVirtualArea";import Loading from "@/components/Loading";import BorderClearOutlinedIcon from "@mui/icons-material/BorderClearOutlined";function View() {const [items, setItems] = useState<any[]>([]);const [hasMore, setHasMore] = useState(true);const loadMoreItems = async () => {// Mock network requestawait new Promise((resolve) =>setTimeout(resolve, 1000 + Math.random() * 1000));// push new itemssetItems((prevItems) => [...prevItems,...Array.from({ length: 10 }, (_, i) => i + prevItems.length),]);// do not load more if there has been 50 items at leastif (items.length + 10 >= 50) {setHasMore(false);}};const renderItem = (item: any) => (<div css={$css`margin-left: 20px`}>{item}</div>);const [loaderRef, loading, _items, render] = useVirtualArea({loadMoreItems,items,hasMore,renderItem,renderNoData: (<div css={$css`display: flex; align-items: center; padding-block: 20px;`}><span>No Data</span><BorderClearOutlinedIcon style={{ marginLeft: "12px" }} /></div>),height: "300px",style: {position: "relative",},loaderComponentProps: {style: {marginBlock: "20px",},},renderTop: () => {return (<divcss={$css`display: flex; align-items: center; position: sticky; top: 0; z-index: 1; background-color: #fff; padding: 10px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);`}><strong>total : </strong><span css={$css`margin-left: 20px;`}>{items.length}</span><strong css={$css`margin-left: 20px;`}>hasMore : </strong><span css={$css`margin-left: 20px;`}>{hasMore.toString()}</span><strong css={$css`margin-left: 20px;`}>loading : </strong><span css={$css`margin-left: 20px;`}>{loading.toString()}</span></div>);},renderLoader: () => {return (<div css={$css`display: flex; align-items: center; margin-left: 12px;`}><Loading on /><span css={$css`margin-left: 20px; color: #44A2FC;`}>Loading Items...</span></div>);},renderUnLoaded: () => {return (<div css={$css`display: flex; align-items: center;`}><span css={$css`color: #333;`}>No more Items</span><spancss={$css`margin-left: 20px;color: #44A2FC;cursor: pointer;`}onClick={() => {setItems([]);setHasMore(true);}}>Restart</span></div>);},});return <div>{render()}</div>;}

useVirtualArea 完整实现:

import React, {useState,useEffect,useRef,useMemo,useCallback,} from "react";export interface VirtualAreaOptions<C extends keyof React.JSX.IntrinsicElements = "div",I extends keyof React.JSX.IntrinsicElements = "div",L extends keyof React.JSX.IntrinsicElements = "div"> {loadMoreItems: () => Promise<void>;items: any[];hasMore: boolean;height: React.CSSProperties["height"];style?: React.CSSProperties;containerComponent?: C;containerComponentProps?: React.JSX.IntrinsicElements[C];renderTop?: React.ReactNode | (() => React.ReactNode);renderItem: React.ReactNode | ((item: any) => React.ReactNode);itemComponent?: I;itemComponentProps?: React.JSX.IntrinsicElements[I];renderNoData?: React.ReactNode | (() => React.ReactNode);renderLoader?: React.ReactNode | (() => React.ReactNode);renderUnLoaded?: React.ReactNode | (() => React.ReactNode);loaderComponent?: L;loaderComponentProps?: React.JSX.IntrinsicElements[L];renderBottom?: React.ReactNode | (() => React.ReactNode);observerOptions?: IntersectionObserverInit;}export function useVirtualArea({loadMoreItems,items,hasMore,height,style: containerStyle,renderTop,renderItem,itemComponent,itemComponentProps,renderNoData,renderLoader,renderUnLoaded,loaderComponent,loaderComponentProps,containerComponent,containerComponentProps,renderBottom,observerOptions,}: VirtualAreaOptions,depths?: any[]) {const [loading, setLoading] = useState(false);const loaderRef = useRef<any>(null);const loadMore = useCallback(async () => {if (loading || !hasMore) return;setLoading(true);await loadMoreItems();setLoading(false);}, [loading, hasMore, loadMoreItems]);useEffect(() => {const options = {root: null,rootMargin: "20px",threshold: 1.0,};const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting) {loadMore();}},{...options,...observerOptions,});if (loaderRef.current) {observer.observe(loaderRef.current);}return () => observer.disconnect();}, [observerOptions, loadMore]);const Container = useMemo(() => containerComponent || "div",[containerComponent]);const Item = useMemo(() => itemComponent || "div", [itemComponent]);const Loader = useMemo(() => loaderComponent || "div", [loaderComponent]);const _containerComponentProps = useMemo(() => {const { style, ...rest } = containerComponentProps ?? {};return {...rest,style: {overflow: "auto",height,...containerStyle,...style,} as React.CSSProperties,};}, [containerComponentProps, height, containerStyle]);const render = useCallback(() => {return (<Container {..._containerComponentProps}>{typeof renderTop === "function" ? renderTop() : renderTop}{/** @ts-ignore */(items || []).length === 0 &&(typeof renderNoData === "function"? renderNoData(): renderNoData === void 0? "No data": renderNoData)}{items.map((item, index) => (<Item key={index} {...itemComponentProps}>{typeof renderItem === "function" ? renderItem(item) : renderItem}</Item>))}{/** @ts-ignore */}<Loader ref={loaderRef} {...loaderComponentProps}>{loading &&(typeof renderLoader === "function"? renderLoader(): renderLoader === void 0? "Loading...": renderLoader)}{!loading &&!hasMore &&(typeof renderUnLoaded === "function"? renderUnLoaded(): renderUnLoaded === void 0? "No more data": renderUnLoaded)}</Loader>{typeof renderBottom === "function" ? renderBottom() : renderBottom}</Container>);}, [_containerComponentProps,renderTop,items,Item,itemComponentProps,renderItem,loaderRef,loaderComponentProps,loading,renderLoader,hasMore,renderUnLoaded,renderBottom,...(depths || []),]);return [loaderRef, loading, items, render] as const;}

Bingo ! 一个用于实现虚拟列表的 useVirtualArea 就这样实现了!

协助本站SEO优化一下,谢谢!
关键词不能为空
同类推荐
«    2026年1月    »
1234
567891011
12131415161718
19202122232425
262728293031
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
搜索
最新留言
文章归档
网站收藏
友情链接