热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

react-virtualized组件的虚拟列表优化分析

本文源码分析基于v9.20.1以及本文demo的测试环境:MacbookPro(Corei72.2G

前言

本文源码分析基于 v9.20.1 以及本文 demo 的测试环境:Macbook Pro(Core i7 2.2G, 16G), Chrome 69,React 16.4.1

在 上一篇 文章中,我简单分析了 react-virtualized 的 List 组件是怎么实现虚拟列表的,在文章的最后,留下了一个问题:怎么尽量避免元素内容重叠的问题?本篇将进行简单分析。

react-virtualized 的 List 组件虽然存在上述所说的问题,但是它还是可以通过和其它组件的组合来做的更好, 尽量避免在渲染图文场景下的元素内容重叠问题。

在 Rendering large lists with React Virtualized 一文中介绍了怎么通过 react-virtualized 来做长列表数据的渲染优化,并详细介绍通过 AutoSizerCellMeasurer 组件来实现 List 组件对列表项动态高度的支持:

  • AutoSizer:可以自动调整其子组件大小(高度和宽度)的高阶组件
  • CellMeasurer:会自动计算组件的大小(高度和宽度)

这篇文章我们就分析一下这两个组件。

AutoSizer

如果不使用 AutoSizer 组件,直接使用 List 组件可能如下:

使用 AutoSizer 组件之后,代码可能变成如下:


  {
    ({width, height}) => (
      
    )
  }

因为 List 组件使用了一个固定高度,所以将 AutoSizerdisableHeight 设置成 true 就相当于告诉 AutoSizer 组件不需要管理子组件的高度。

AutoSizer 的实现也比较简单,先看起 render 方法:

// source/AutoSizer/AutoSizer.js

// ...
  render() {
    const {
      children,
      className,
      disableHeight,
      disableWidth,
      style,
    } = this.props;
    const {height, width} = this.state;

    // 外部 div 的样式,外部 div 不需要设置高宽
    // 而内部组件应该使用被计算后的高宽值
    // https://github.com/bvaughn/react-virtualized/issues/68
    const outerStyle: Object = {overflow: 'visible'};
    const childParams: Object = {};

    if (!disableHeight) {
      outerStyle.height = 0;
      childParams.height = height;
    }

    if (!disableWidth) {
      outerStyle.width = 0;
      childParams.width = width;
    }

    return (
      
{children(childParams)}
); } // ... _setRef = (autoSizer: ?HTMLElement) => { this._autoSizer = autoSizer; }; // ...

然后再看下 componentDidMount 方法:

// source/AutoSizer/AutoSizer.js

// ...

  componentDidMount() {
    const {nonce} = this.props;
    // 这里的每一个条件都可能是为了修复某一个边界问题(edge-cases),如 #203 #960 #150 etc.
    if (
      this._autoSizer &&
      this._autoSizer.parentNode &&
      this._autoSizer.parentNode.ownerDocument &&
      this._autoSizer.parentNode.ownerDocument.defaultView &&
      this._autoSizer.parentNode instanceof
        this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement
    ) {
      // 获取父节点
      this._parentNode = this._autoSizer.parentNode;

      // 创建监听器,用于监听元素大小的变化
      this._detectElementResize = createDetectElementResize(nonce);
      // 设置需要被监听的节点以及回调处理
      this._detectElementResize.addResizeListener(
        this._parentNode,
        this._onResize,
      );

      this._onResize();
    }
  }
  
  // ...

componentDidMount 方法中,主要创建了监听元素大小变化的监听器。 createDetectElementResize 方法( 源代码 )是基于 Javascript-detect-element-resize 实现的,针对 SSR 的支持更改了一些代码。接下来看下 _onResize 的实现:

// source/AutoSizer/AutoSizer.js

// ...
  _OnResize= () => {
    const {disableHeight, disableWidth, onResize} = this.props;

    if (this._parentNode) {
      // 获取节点的高宽    
      const height = this._parentNode.offsetHeight || 0;
      const width = this._parentNode.offsetWidth || 0;
      
      const style = window.getComputedStyle(this._parentNode) || {};
      const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
      const paddingRight = parseInt(style.paddingRight, 10) || 0;
      const paddingTop = parseInt(style.paddingTop, 10) || 0;
      const paddingBottom = parseInt(style.paddingBottom, 10) || 0;
      
      // 计算新的高宽
      const newHeight = height - paddingTop - paddingBottom;
      const newWidth = width - paddingLeft - paddingRight;

      if (
        (!disableHeight && this.state.height !== newHeight) ||
        (!disableWidth && this.state.width !== newWidth)
      ) {
        this.setState({
          height: height - paddingTop - paddingBottom,
          width: width - paddingLeft - paddingRight,
        });

        onResize({height, width});
      }
    }
  };
// ...

_onResize 方法做的事就是计算元素新的高宽,并更新 state ,触发 re-render 。接下来看看 CellMeasurer 组件的实现。

CellMeasurer

CellMeasurer 组件会根据自身的内容自动计算大小,需要配合 CellMeasurerCache 组件使用,这个组件主要缓存已计算过的 cell 元素的大小。

先修改一下代码,看看其使用方式:

import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized";

class App extends Component {
  constructor() {
    ...
    this.cache = new CellMeasurerCache({
      fixedWidth: true, 
      defaultHeight: 180
    });
  }
  ...
}

首先,我们创建了 CellMeasurerCache 实例,并设置了两个属性:

  • fixedWidth:表示 cell 元素是固定宽度的,但高度是动态的
  • defaultHeight:未被渲染的 cell 元素的默认高度(或预估高度)

然后,我们需要修改 List 组件的 renderRow 方法以及 List 组件:

// ...
  renderRow({ index, key, style, parent }) {
   // 上一篇分析过,List 是 columnCount 为 1 的 Grid 组件,
   // 因而 columnIndex 是固定的 0
    return (
      
          
{ // 省略 }
); } // ... { ({width, height}) => ( ) }

对于 List 组件有三个变动:

  1. rowHeight 属性的值变成 this.cache.rowHeight
  2. 新增了 deferredMeasurementCache 属性,并且其值为 CellMeasurerCache 的实例
  3. renderRow 方法返回的元素外用 CellMeasurer 组件包裹了一层

List 组件的 文档 看,并没有 deferredMeasurementCache 属性说明,但在上一篇文章分析过, List 组件的内部实现是基于 Grid 组件的:

// source/List/List.js

// ...

render() {
  //...

  return (
    
  );
}

// ...

Grid 组件是拥有这个属性的,其值是 CellMeasurer 实例,因而这个属性实际上是传递给了 Grid 组件。

回到 CellMeasurer 组件,其实现是比较简单的:

// source/CellMeasurer/CellMeasurer.js

// ...
  componentDidMount() {
    this._maybeMeasureCell();
  }

  componentDidUpdate() {
    this._maybeMeasureCell();
  }

  render() {
    const {children} = this.props;

    return typeof children === 'function'
      ? children({measure: this._measure})
      : children;
  }
// ...

上述代码非常简单, render 方法只做子组件的渲染,并在组件挂载和更新的时候都去调用 _maybeMeasureCell 方法,这个方法就会去计算 cell 元素的大小了:

// source/CellMeasurer/CellMeasurer.js

// ...
  // 获取元素的大小
  _getCellMeasurements() {
    // 获取 CellMeasurerCache 实例
    const {cache} = this.props;
    
    // 获取组件自身对应的 DOM 节点
    const node = findDOMNode(this);

    if (
      node &&
      node.ownerDocument &&
      node.ownerDocument.defaultView &&
      node instanceof node.ownerDocument.defaultView.HTMLElement
    ) {
      // 获取节点对应的大小
      const styleWidth = node.style.width;
      const styleHeight = node.style.height;

      /**
      * 创建 CellMeasurerCache 实例时,如果设置了 fixedWidth 为 true,
      * 则 hasFixedWidth() 返回 true;如果设置了 fixedHeight 为 true,
      * 则 hasFixedHeight() 返回 true。两者的默认值都是 false
      * 将 width 或 heigth 设置成 auto,便于得到元素的实际大小
      **/
      if (!cache.hasFixedWidth()) {
        node.style.width = 'auto';
      }
      if (!cache.hasFixedHeight()) {
        node.style.height = 'auto';
      }

      const height = Math.ceil(node.offsetHeight);
      const width = Math.ceil(node.offsetWidth);
      
      // 获取到节点的实际大小之后,需要重置样式
      // https://github.com/bvaughn/react-virtualized/issues/660
      if (styleWidth) {
        node.style.width = styleWidth;
      }
      if (styleHeight) {
        node.style.height = styleHeight;
      }

      return {height, width};
    } else {
      return {height: 0, width: 0};
    }
  }
  
  _maybeMeasureCell() {
    const {
      cache,
      columnIndex = 0,
      parent,
      rowIndex = this.props.index || 0,
    } = this.props;
    
    // 如果缓存中没有数据
    if (!cache.has(rowIndex, columnIndex)) {
      // 则计算对应元素的大小
      const {height, width} = this._getCellMeasurements();
        
      // 缓存元素的大小    
      cache.set(rowIndex, columnIndex, width, height);
      
      // 通过上一篇文章的分析,可以得知 parent 是 Grid 组件
      // 更新 Grid 组件的 _deferredInvalidate[Column|Row]Index,使其在挂载或更新的时候 re-render
      if (
        parent &&
        typeof parent.invalidateCellSizeAfterRender === 'function'
      ) {
        parent.invalidateCellSizeAfterRender({
          columnIndex,
          rowIndex,
        });
      }
    }
  }
// ...

_maybeMeasureCell 方法最后会调用 invalidateCellSizeAfterRender ,从方法的 源代码 上看,它只是更新了组件的 _deferredInvalidateColumnIndex_deferredInvalidateRowIndex 的值,那调用它为什么会触发 Grid 的 re-render 呢?因为这两个值被用到的地方是在 _handleInvalidatedGridSize 方法中,从其 源代码 上看,它调用了 recomputeGridSize 方法(后文会提到这个方法)。而 _handleInvalidatedGridSize 方法是在组件的 componentDidMountcomponentDidUpdate 的时候均会调用。

从上文可以知道,如果子组件是函数,则调用的时候还会传递 measure 参数,其值是 _measure ,实现如下:

// source/CellMeasurer/CellMeasurer.js

// ...

  _measure = () => {
    const {
      cache,
      columnIndex = 0,
      parent,
      rowIndex = this.props.index || 0,
    } = this.props;
    
    // 计算对应元素的大小
    const {height, width} = this._getCellMeasurements();
    
    // 对比缓存中的数据
    if (
      height !== cache.getHeight(rowIndex, columnIndex) ||
      width !== cache.getWidth(rowIndex, columnIndex)
    ) {
      // 如果不相等,则重置缓存
      cache.set(rowIndex, columnIndex, width, height);
      
      // 并通知父组件,即 Grid 组件强制 re-render    
      if (parent && typeof parent.recomputeGridSize === 'function') {
        parent.recomputeGridSize({
          columnIndex,
          rowIndex,
        });
      }
    }
  };
  
// ...

recomputeGridSize 方法时 Grid 组件的一个公开方法,用于重新计算元素的大小,并通过 forceUpdate 强制 re-render,其实现比较简单,如果你有兴趣了解,可以去查看下其 源代码 。

至此, CellMeasurer 组件的实现就分析完结了。如上文所说, CellMeasurer 组件要和 CellMeasurerCache 组件搭配使用,因而接下来我们快速看下 CellMeasurerCache 组件的实现:

// source/CellMeasurer/CellMeasurerCache.js

// ...
// KeyMapper 是一个函数,根据行索引和列索引返回对应数据的唯一 ID
// 这个 ID 会作为 Cache 的 key
// 默认的唯一标识是 `${rowIndex}-${columnIndex}`,见下文的 defaultKeyMapper
type KeyMapper = (rowIndex: number, columnIndex: number) => any;

export const DEFAULT_HEIGHT = 30;
export const DEFAULT_WIDTH = 100;

// ...

type Cache = {
  [key: any]: number,
};

  // ...
  
  _cellHeightCache: Cache = {};
  _cellWidthCache: Cache = {};
  _columnWidthCache: Cache = {};
  _rowHeightCache: Cache = {};
  _columnCount = 0;
  _rowCount = 0;
  // ...
  
  constructor(params: CellMeasurerCacheParams = {}) {
    const {
      defaultHeight,
      defaultWidth,
      fixedHeight,
      fixedWidth,
      keyMapper,
      minHeight,
      minWidth,
    } = params;
    
    // 保存相关值或标记位
    this._hasFixedHeight = fixedHeight === true;
    this._hasFixedWidth = fixedWidth === true;
    this._minHeight = minHeight || 0;
    this._minWidth = minWidth || 0;
    this._keyMapper = keyMapper || defaultKeyMapper;
    
    // 获取默认的高宽
    this._defaultHeight = Math.max(
      this._minHeight,
      typeof defaultHeight === 'number' ? defaultHeight : DEFAULT_HEIGHT,
    );
    this._defaultWidth = Math.max(
      this._minWidth,
      typeof defaultWidth === 'number' ? defaultWidth : DEFAULT_WIDTH,
    );

    // ...
  }

  // ...
      
  hasFixedHeight(): boolean {
    return this._hasFixedHeight;
  }

  hasFixedWidth(): boolean {
    return this._hasFixedWidth;
  }
  
  // ...
  // 根据索引获取对应的列宽
  // 可用于 Grid 组件的 columnWidth 属性
  columnWidth = ({index}: IndexParam) => {
    const key = this._keyMapper(0, index);

    return this._columnWidthCache.hasOwnProperty(key)
      ? this._columnWidthCache[key]
      : this._defaultWidth;
  };
  
  // ...
  
  // 根据行索引和列索引获取对应 cell 元素的高度    
  getHeight(rowIndex: number, columnIndex: number = 0): number {
    if (this._hasFixedHeight) {
      return this._defaultHeight;
    } else {
      const key = this._keyMapper(rowIndex, columnIndex);

      return this._cellHeightCache.hasOwnProperty(key)
        ? Math.max(this._minHeight, this._cellHeightCache[key])
        : this._defaultHeight;
    }
  }
  
  // 根据行索引和列索引获取对应 cell 元素的宽度  
  getWidth(rowIndex: number, columnIndex: number = 0): number {
    if (this._hasFixedWidth) {
      return this._defaultWidth;
    } else {
      const key = this._keyMapper(rowIndex, columnIndex);

      return this._cellWidthCache.hasOwnProperty(key)
        ? Math.max(this._minWidth, this._cellWidthCache[key])
        : this._defaultWidth;
    }
  }
  
  // 是否有缓存数据
  has(rowIndex: number, columnIndex: number = 0): boolean {
    const key = this._keyMapper(rowIndex, columnIndex);

    return this._cellHeightCache.hasOwnProperty(key);
  }
  
  // 根据索引获取对应的行高
  // 可用于 List/Grid 组件的 rowHeight 属性
  rowHeight = ({index}: IndexParam) => {
    const key = this._keyMapper(index, 0);

    return this._rowHeightCache.hasOwnProperty(key)
      ? this._rowHeightCache[key]
      : this._defaultHeight;
  };
    
  // 缓存元素的大小   
  set(
    rowIndex: number,
    columnIndex: number,
    width: number,
    height: number,
  ): void {
    const key = this._keyMapper(rowIndex, columnIndex);

    if (columnIndex >= this._columnCount) {
      this._columnCount = columnIndex + 1;
    }
    if (rowIndex >= this._rowCount) {
      this._rowCount = rowIndex + 1;
    }

    // 缓存单个 cell 元素的高宽
    this._cellHeightCache[key] = height;
    this._cellWidthCache[key] = width;
    
    // 更新列宽或行高的缓存
    this._updateCachedColumnAndRowSizes(rowIndex, columnIndex);
  }
  
  // 更新列宽或行高的缓存,用于纠正预估值的计算 
  _updateCachedColumnAndRowSizes(rowIndex: number, columnIndex: number) {
    if (!this._hasFixedWidth) {
      let columnWidth = 0;
      for (let i = 0; i  
 

对于 _updateCachedColumnAndRowSizes 方法需要补充说明一点的是,通过上一篇文章的分析,我们知道在组件内不仅需要去计算总的列宽和行高的( CellSizeAndPositionManager#getTotalSize 方法) ,而且需要计算 cell 元素的大小( CellSizeAndPositionManager#_cellSizeGetter 方法)。在 cell 元素被渲染之前,用的是预估的列宽值或者行高值计算的,此时的值未必就是精确的,而当 cell 元素渲染之后,就能获取到其真实的大小,因而缓存其真实的大小之后,在组件的下次 re-render 的时候就能对原先预估值的计算进行纠正,得到更精确的值。

demo的完整代码戳此: ReactVirtualizedList

总结

List 组件通过和 AutoSizer 组件以及 CellMeasurer 组件的组合使用,很好的优化了 List 组件自身对元素动态高度的支持。但从上文分析可知, CellMeasurer 组件会在其初次挂载( mount )和更新( update )的时候通过 _maybeMeasureCell 方法去更新自身的大小,如果 cell 元素只是渲染纯文本,这是可以满足需求的,但 cell 元素是渲染图文呢?

因为图片存在网络请求,因而在组件挂载和更新时,图片未必就一定加载完成了,因而此时获取到的节点大小是不准确的,就有可能导致内容重叠:

react-virtualized 组件的虚拟列表优化分析

这种情况下,我们可以根据项目的实际情况做一些布局上的处理,比如去掉 border ,适当增加 cell 元素的 padding 或者 margin 等( :blush: :blush: :blush: ),这是有点取巧的方式,那不取的方式是 CellMeasurer 的子组件换成函数

上文已经说过,如果子组件是函数,则调用的时候会传递一个函数 measure 作为参数,这个函数所做的事情就是重新计算对应 cell 元素的大小,并使 Grid 组件 re-render。因而,我们可以将这个参数绑定到 imgonLoad 事件中,当图片加载完成时,就会重新计算对应 cell 元素的大小,此时,获取到的节点大小就是比较精确的值了:

// ...
  renderRow({ index, key, style, parent }) {
   // 上一篇分析过,List 是 columnCount 为 1 的 Grid 组件,
   // 因而 columnIndex 是固定的 0
    return (
      
          {
            ({measure}) => (
              
{`${text}`}
) }
); } // ...

渲染图文demo的完整代码戳此: ReactVirtualizedList with image

<本文完>

参考

  • Rendering large lists with React Virtualized

以上所述就是小编给大家介绍的《react-virtualized 组件的虚拟列表优化分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • 本文介绍了一种使用 JavaScript 计算两个日期之间时间差的方法。该方法支持多种时间格式,并能返回秒、分钟、小时和天数等不同精度的时间差。 ... [详细]
  • 在处理大规模数据数组时,优化分页组件对于提高页面加载速度和用户体验至关重要。本文探讨了如何通过高效的分页策略,减少数据渲染的负担,提升应用性能。具体方法包括懒加载、虚拟滚动和数据预取等技术,这些技术能够显著降低内存占用和提升响应速度。通过实际案例分析,展示了这些优化措施的有效性和可行性。 ... [详细]
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • 微软推出Windows Terminal Preview v0.10
    微软近期发布了Windows Terminal Preview v0.10,用户可以在微软商店或GitHub上获取这一更新。该版本在2月份发布的v0.9基础上,新增了鼠标输入和复制Pane等功能。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 单元测试:使用mocha和should.js搭建nodejs的单元测试
    2019独角兽企业重金招聘Python工程师标准BDD测试利器:mochashould.js众所周知对于任何一个项目来说,做好单元测试都是必不可少 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • V8不仅是一款著名的八缸发动机,广泛应用于道奇Charger、宾利Continental GT和BossHoss摩托车中。自2008年以来,作为Chromium项目的一部分,V8 JavaScript引擎在性能优化和技术创新方面取得了显著进展。该引擎通过先进的编译技术和高效的垃圾回收机制,显著提升了JavaScript的执行效率,为现代Web应用提供了强大的支持。持续的优化和创新使得V8在处理复杂计算和大规模数据时表现更加出色,成为众多开发者和企业的首选。 ... [详细]
  • 利用爬虫技术抓取数据,结合Fiddler与Postman在Chrome中的应用优化提交流程
    本文探讨了如何利用爬虫技术抓取目标网站的数据,并结合Fiddler和Postman工具在Chrome浏览器中的应用,优化数据提交流程。通过详细的抓包分析和模拟提交,有效提升了数据抓取的效率和准确性。此外,文章还介绍了如何使用这些工具进行调试和优化,为开发者提供了实用的操作指南。 ... [详细]
  • 本文介绍了 Vue 开发的入门指南,重点讲解了开发环境的配置与项目的基本搭建。推荐使用 WebStorm 作为 IDE,其下载地址为 。安装时请选择适合您操作系统的版本,并通过 获取激活码。WebStorm 是前端开发者的理想选择,提供了丰富的功能和强大的代码编辑能力。 ... [详细]
  • 技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告
    技术日志:使用 Ruby 爬虫抓取拉勾网职位数据并生成词云分析报告 ... [详细]
  • 每日前端实战:148# 视频教程展示纯 CSS 实现按钮两侧滑入装饰元素的悬停效果
    通过点击页面右侧的“预览”按钮,您可以直接在当前页面查看效果,或点击链接进入全屏预览模式。该视频教程展示了如何使用纯 CSS 实现按钮两侧滑入装饰元素的悬停效果。视频内容具有互动性,观众可以实时调整代码并观察变化。访问以下链接体验完整效果:https://codepen.io/comehope/pen/yRyOZr。 ... [详细]
  • Java环境中Selenium Chrome驱动在大规模Web应用扩展时的性能限制分析 ... [详细]
author-avatar
卖火柴的杜拉拉
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有