虚拟化可以定义为创建某物的虚拟版本。
这是关于 React 中的虚拟化的两篇博文中的第一篇
虚拟化可以定义为创建某物的虚拟版本。在 React 上下文中,它可以定义为创建一个窗口,允许记录在用户滚动时滑入和滑出视图。虚拟化确实有助于大大减少渲染列表所需的时间,特别是在应用于长列表时。这种技术可以提高 Web 应用程序的性能。对于虚拟化,最重要的是只有视口内的那些元素才会加载到虚拟 DOM 中。 React 依赖于 Virtual DOM 来有效地仅渲染已更新的组件。
请记住,UI 的这种表示随后会与“真实”DOM 使用 差异算法 .这是由 ReactDOM 库在 React 中制作的。
DOM 更新可能会导致各种问题(还有很多其他问题):
- 初始渲染缓慢
- 延迟滚动
多亏了虚拟化,我们面临着这两个问题。尽管 React 引入了所有优化,但对于高性能应用程序,我们可能需要更多。
DOM 重排 & DOM 重绘
DOM reflow 和 DOM repaint 是两个复杂的过程,它们更好地解释了升级真实 DOM 的成本。
DOM 重排 是计算视口中每个元素、所有子元素及其相邻元素的位置和大小的过程。这是一个非常昂贵的过程。
DOM 重绘 是将树中的节点转换为屏幕上的像素的过程。这就是可见性的归档方式。
每次要进行更新时都会触发这两个过程,这使得更新 DOM 对于 Web 浏览器来说代价高昂。
反应窗口
反应窗口 作为一个开源解决方案来解决当大量元素必须在屏幕上显示时引起的问题,解决初始渲染缓慢和滚动滞后的问题。同时,有助于虚拟化,这是高性能 React 应用程序的最大问题之一。
根据其创建者 Brian Vaughn 的说法,react-window 完全重写了 反应虚拟化 (您可能听说过),速度更快,捆绑器尺寸更小。它包含反应虚拟化的所有基本功能。
React-window 可以与其他相关库一起使用,最大限度地提高其性能。这是我发现的两个最有用的。
- Infinite Loader - 避免在需要之前获取数据(直到您滚动以适应更多数据)
- Autosizer - 计算可用视口以调整适合视图的元素数量
我们开始做吧
让我们实现一个简单的虚拟化列表。要一起使用它,首先您必须将以下依赖项添加到您的项目中(如果您的项目使用 Typescript,请记住添加类型):
yarn add react-window react-virtualized-auto-sizer react-window-infinite-loader
纱线添加 -D @types/react-window @types/react-virtualized-auto-sizer @types/react-window-infinite-loader
虚拟列表组件
虚拟列表组件很复杂,但是一旦我理解了你就必须正确地使用它来充分利用它。
进口 {
克隆元素,
CSS属性,
前向引用,
反应元素,
使用效果,
使用参考,
} 来自“反应”;
从 'react-window' 导入 { FixedSizeListProps, VariableSizeList };
从 'react-window-infinite-loader' 导入 InfiniteLoader;
从 'react-virtualized-auto-sizer' 导入 AutoSizer; 导出接口 VirtualListProps
扩展省略
FixedSizeListProps,
| '孩子们'
| '内部元素类型'
| '高度'
| '宽度'
| '项目大小'
| '项目计数'
> {
hasNextPage: 布尔值;
isNextPageLoading:布尔值;
项目:未知[];
getItemsHeight?: (index: number) => number;
itemSize?: 数字;
loadNextPage: () => 无效;
行:反应元素;
} 导出接口 VirtualListItemProps {
索引号;
风格:记录<string, unknown>;
} /**
* 虚拟列表的内部组件。这就是“魔法”。
* 捕获顶部元素位置并将其应用于列表。
**/
const 内部 = forwardRef <HTMLDivElement, React.HTMLProps<HTMLDivElement> >(
功能内部({孩子,...休息},参考){
返回 (
<div {...rest} ref={ref}>
<ul>{孩子们}</ul>
</div>
);
}
); 函数虚拟列表行({
排,
isItemLoaded,
指数,
风格,
}: {
isItemLoaded: (index: number) => boolean;
行:反应元素;
索引号;
样式:CSS属性;
}) {
if (!isItemLoaded(index)) 返回<div></div>;
return cloneElement(Row, { index, style });
} 导出默认函数 VirtualList({
// 是否还有更多要加载的项目?
有下一页, // 我们当前是否正在加载项目页面?
isNextPageLoading, // 到目前为止加载的项目数组。
项目, // 负责加载下一页项的回调函数。
加载下一页,
获取项目高度,
项目大小,
排,
}: 虚拟列表属性){
使用效果(()=> {
if (listRef && listRef.current && listRef.current.resetAfterIndex) {
listRef.current.resetAfterIndex(0);
}
}, [项目]); 常量 listRef: any = useRef({});
// 如果还有更多要加载的项目,则添加一个额外的行来保存加载指示器。
常量 itemCount = hasNextPage ? items.length + 1 : items.length; // 一次只加载 1 页项目。
// 向 InfiniteLoader 传递一个空回调,以防它要求我们多次加载。
// eslint-disable-next-line @typescript-eslint/no-empty-function
常量 loadMoreItems = isNextPageLoading ? () => {} : loadNextPage; const getItemHeight = (index: number) => {
if (getItemsHeight) return getItemsHeight(index); 返回项目大小 || 40;
}; // 除了我们的加载指示行之外,每一行都被加载。
const isItemLoaded = (index: number) => !hasNextPage ||索引 < 项目长度; 返回 (
<AutoSizer>
{({ 高度, 宽度}) => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
加载更多项目={加载更多项目}
>
{({ onItemsRendered }) => (
<VariableSizeList
高度={高度}
itemCount={itemCount}
itemSize={getItemHeight}
onItemsRendered={onItemsRendered}
参考={列表参考}
宽度={宽度}
innerElementType={内部}
>
{({ 索引,样式}) => (
<VirtualListRow
isItemLoaded={isItemLoaded}
行={行}
索引={索引}
风格={风格}
/>
)}
</VariableSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
);
}
让我们分解一下:
- 我已经定义了 虚拟列表道具 我省略的界面(省略<Type, Keys>.与 Pick 不同,从类型的所有属性中删除所选键),还定义以下参数
- hasNextPage → 一个布尔值,设置是否有更多结果可供加载。此信息通常来自最近的 API 请求
- isNextPageLoading → 一个布尔值,用于了解当前是否正在获取新结果。例如,这可能是 Redux 商店中的飞行标志,也可以由 swr 提供
- items → 到目前为止加载的项目数组。它的类型是未知的,因为程序员有责任确定它的类型
- getItemsHeight → 获取项目近似高度的函数
- 项目大小? → 物品的准确高度(如果可能的话)
- loadNextPage → 需要加载更多项目时触发的回调
- 行 → 包含项目内容的 ReactElement
- VirtualListItemProps 包含每个项目行的属性。包括索引(要在列表中搜索)和正确绘制它的样式。
- 这 内 组件基本上包装了列表
- 这 虚拟列表行 组件为每个现有元素创建一行(意味着它有一个索引)
- 这 虚拟列表 默认导出的组件本身将 Autosizer、变量列表和虚拟行放在一起。它还计算项目高度并在必要时触发回调。
获取项目列表的功能
我在用 swr/无限 从我在互联网上找到的虚假 API 中获取无限的项目列表。我创建了一个自定义钩子,它返回构造列表所需的所有内容。我们也可以返回一个属性来知道是否有下一页,但使用 API 我知道它会。
从“axios”导入axios;
从“swr/infinite”导入useSWRInfinite; 从“../model/user”导入{ IUserBase }; 常量 SWR_KEY = "用户"; 导出接口 IUseUserList {
列表?:IUserBase[];
总数;
错误?:错误;
加载:布尔值;
尺寸:数量;
setSize: (arg0: number) => void;
} 导出 const useUserList = (): IUseUserList => {
const getKey = (pageIndex: number, previousPageData: string | any[]) => {
// 检查是否已经到达终点
if (previousPageData && !previousPageData.length) 返回 null;
返回`/${SWR_KEY}?page=${pageIndex}`;
}; const fetcher = (): any => {
return axios.get(`https://gorest.co.in/public/v2/${SWR_KEY}?page=${size}`).then((res) => res.data);
}; 常量 {
数据:列表,
错误,
正在验证,
尺寸,
设置大小,
} = useSWRInfinite(getKey, fetcher, {
重新验证第一页:假,
重新验证所有:真,
}); 返回 {
列表:(列表|| []).flat(),
总计:列表?列表[0].总数:0,
错误,
加载:isValidating,
尺寸,
设置大小,
};
};
使用它的组件
我创建了一个容器来包装数据获取功能,该功能直接调用显示列表的组件。
UserItem 使用 VirtualListItemProps 来获取将每个项目正确放置在屏幕上所需的索引和样式。
const UserItem = ({ index, style, ...props }: any) => {
const user = props.list[index]; 返回 (
<Wrapper style={style}>
<Content>
<h4>
<b>{用户名}</b>
<Badge status={user.status}>{用户.状态}</Badge>
</h4>
<p>{`${user.email} | ${user.gender.charAt(0).toUpperCase()}${user.gender.slice(1)}`}</p>
</Content>
</Wrapper>
);
};
首先,我们得到项目,然后我们渲染我们的项目。您可以查看完整的组件代码 这里 .
结果如下:
最后的话
虚拟化起初可能看起来具有挑战性和复杂性,但一旦您了解了它的工作原理。我强烈建议您花时间实现自己的虚拟化列表,使用 react-virtualized 或其他库,因为这个复杂的概念将帮助您了解性能最佳的应用程序可能实现的功能。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明