列表虚拟化

在本指南中,讨论列表虚拟化(也称为窗口化)。

这是在动态列表中仅呈现可见内容行而不是整个列表的想法。

呈现的行只是完整列表的一小部分,随着用户滚动可见(窗口)移动。 这可以提高渲染性能。

如果使用 React 并且需要高效地显示大量数据列表,
您可能熟悉 react-virtualized
它是 Brian Vaughn 的一个窗口库,它只呈现列表中当前可见的项目(在滚动“视口”中)。
这意味着无需支付一次呈现数千行数据的成本。
这篇文章随附了带有 react-window 的列表虚拟化视频演练。


列表虚拟化如何工作?

“虚拟化”项目列表涉及维护一个窗口并在列表中移动该窗口。

通过以下方式在反应虚拟化工作中开窗:

  • 具有相对定位(窗口)的小型容器 DOM 元素(例如 <ul>

  • 有一个用于滚动的大 DOM 元素

  • 绝对将孩子定位在容器内,设置他们的顶部、左侧、宽度和高度的样式

虚拟化不是一次渲染列表中的 1000 个元素(这会导致初始渲染变慢或影响滚动性能),

而是专注于仅渲染用户可见的项目。

这有助于在中低端设备上保持快速的列表渲染。

可以在用户滚动时获取/显示更多项目,卸载以前的条目并用新条目替换它们。


react-virtualized 的较小替代方案

react-window 是同一作者对 react-virtualized 的重写,旨在更小、更快和更易摇树

使用它代替 react-virtualized 可以节省大约 20-30KB(gzipped):

这两个包的 API 相似,但在不同的地方,react-window 往往更简单。

react-window 的组件包括:

List

列表呈现元素的窗口列表(行),这意味着仅向用户显示可见行(例如 FixedSizeList、VariableSizeList)。

列表使用 Grid(内部)来呈现行,将道具中继到该内部网格。

使用 React 渲染数据列表

这是使用 React 渲染简单数据列表 (itemsArray) 的示例:

import React from "react";
import ReactDOM from "react-dom";

const itemsArray = [
  { name: "Drake" },
  { name: "Halsey" },
  { name: "Camillo Cabello" },
  { name: "Travis Scott" },
  { name: "Bazzi" },
  { name: "Flume" },
  { name: "Nicki Minaj" },
  { name: "Kodak Black" },
  { name: "Tyga" },
  { name: "Buno Mars" },
  { name: "Lil Wayne" }, ...
]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <div
    style=
    class="List"
  >
    {itemsArray.map((item, index) => Row({ index }))}
  </div>
);

ReactDOM.render(<Example />, document.getElementById("root"));

使用 react-window 渲染列表

…这里是使用 react-windowFixedSizeList 的相同示例,它需要一些道具(width、height、itemCount、itemSize)和作为子项传递的行渲染函数:

import React from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from "react-window";

const itemsArray = [...]; // our data

const Row = ({ index, style }) => (
  <div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
    {itemsArray[index].name}
  </div>
);

const Example = () => (
  <List
    className="List"
    height={150}
    itemCount={itemsArray.length}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

ReactDOM.render(<Example />, document.getElementById("root"));

Grid

Grid 使用虚拟化沿垂直和水平轴呈现表格数据(例如 FizedSizeGridVariableSizeGid)。

它仅根据当前的水平/垂直滚动位置呈现填充自身所需的 Grid 单元格。

如果想用 Grid 布局呈现与之前相同的列表,假设我们的输入是一个多维数组,可以使用 FixedSizeGrid 完成此操作,如下所示:

import React from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeGrid as Grid } from 'react-window';

const itemsArray = [
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
  [{},{},{},...],
]; 

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div
    className={
      columnIndex % 2
        ? rowIndex % 2 === 0
          ? 'GridItemOdd'
          : 'GridItemEven'
        : rowIndex % 2
          ? 'GridItemOdd'
          : 'GridItemEven'
    }
    style={style}
  >
    {itemsArray[rowIndex][columnIndex].name}
  </div>
);

const Example = () => (
  <Grid
    className="Grid"
    columnCount={5}
    columnWidth={100}
    height={150}
    rowCount={5}
    rowHeight={35}
    width={300}
  >
    {Cell}
  </Grid>
);

ReactDOM.render(<Example />, document.getElementById('root'));

更深入的 react-window 示例

Scott Taylor 使用 react-windowFixedSizeGrid 实现了一个开源的 Pitchfork 音乐评论抓取工具 (src)。

Pitchfork scraper 使用 react-window-infinite-loader,它有助于将大型数据集分解为可以在滚动到视图中时加载的块。

以下是如何将 react-window-infinite-loader 纳入此应用程序的片段:

import React, { Component } from 'react';
import { FixedSizeGrid as Grid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
...
  render() {
    return (
      <InfiniteLoader
        isItemLoaded={this.isItemLoaded}
        loadMoreItems={this.loadMoreItems}
        itemCount={this.state.count + 1}
      >
        {({ onItemsRendered, ref }) => (
          <Grid
            onItemsRendered={this.onItemsRendered(onItemsRendered)}
            columnCount={COLUMN_SIZE}
            columnWidth={180}
            height={800}
            rowCount={Math.max(this.state.count / COLUMN_SIZE)}
            rowHeight={220}
            width={1024}
            ref={ref}
          >
            {this.renderCell}
          </Grid>
        )}
      </InfiniteLoader>
    );
  }
}

您可能会发现从 react-virtualized 移植应用程序的提交很有用。

使用 FixedSizeListPitchfork 抓取器的实现也是可用的(Pixel 上的 demo):

这是实现的一个片段:

 return (
      <InfiniteLoader
        isItemLoaded={this.isItemLoaded}
        loadMoreItems={this.loadMoreItems}
        itemCount={this.state.count}
      >
        {({ onItemsRendered, ref }) => (
          <section>
            <FixedSizeList
            itemCount={this.state.count}
            itemSize={ROW_HEIGHT}
            onItemsRendered={onItemsRendered}
            height={this.state.height}
            width={this.state.width}
            ref={ref}
            >
              {this.renderCell}
            </FixedSizeList>
          </section>
        )}
      </InfiniteLoader>
    );

如果我们对 Grid 虚拟化解决方案有更复杂的需求怎么办?

我们发现了一个 The Movie Database 演示应用程序,它在后台使用了 react-virtualizedInfinite Loader

将它移植到 react-window 和 react-window-infinite-loader 并没有花很长时间,但我们确实发现一些组件尚不受支持。 无论如何,最终的功能非常接近。

将它移植到 react-windowreact-window-infinite-loader 并没有花很长时间,但我们确实发现一些组件尚不受支持。 无论如何,最终的功能非常接近。

缺少的组件是 WindowScrollerAutoSizer……我们接下来会看到它们。

...
    return (
      <section>
        <AutoSizer disableHeight>
          {({width}) => {
            const {movies, hasMore} = this.props;
            const rowCount = getRowsAmount(width, movies.length, hasMore);
            ...
            return (
              <InfiniteLoader
                ref={this.infiniteLoaderRef}
                ...
                {({onRowsRendered, registerChild}) => (
                  <WindowScroller>
                    {({height, scrollTop}) => (

react-window 缺少什么?

react-window 还没有 react-virtualized 的完整 API 表面,所以如果考虑它,请检查比较文档。 缺少了什么?

  • WindowScroller - 这是一个 react-virtualized 组件,可以根据窗口的滚动位置滚动列表。 目前没有计划为 react-window 实现此功能,因此您需要在用户空间中解决此问题。

  • AutoSizer - HOC 可以适应所有可用空间,自动调整单个孩子的宽度和高度。 Brian 将其作为一个独立的包来实现。 关注此问题以获取最新信息。

  • CellMeasurer - HOC 通过以用户不可见的方式呈现单元格的内容来自动测量单元格的内容。 在此处关注有关支持的讨论。

也就是说,我们发现 react-window 开箱即用,足以满足我们的大部分需求。


网络平台的改进

一些现代浏览器现在支持 CSS 内容可见性。

content-visibility:auto 允许您跳过渲染和绘制屏幕外内容,直到需要为止。

如果您的 HTML 文档很长且渲染成本很高,请考虑尝试使用该属性。

对于渲染动态内容列表,我仍然建议使用像 react-window 这样的库。

很难拥有这样一个库的 content-visbility:hidden 版本,它可以像今天许多列表虚拟化库那样在屏幕外积极使用 display:none 或删除 DOM 节点来击败一个版本。


进一步阅读

如需进一步了解 react-windowreact-virtualized,请查看:


知识点