手写长列表解决方案

274 阅读3分钟

场景

长列表的渲染,是前端很容易遇到的问题。常规的解决方案有滚动加载,分页等等。但是常规的滚动加载(没有做列表虚拟化),随着往下滚动加载的越多,div节点的数目也会变得更多,从而导致界面的性能问题,而分页加载虽然避开了性能问题,但体验没有滚动加载那么顺滑。所以是虚拟化的列表是较好的选择,它只会渲染视图范围内的div节点个数。

解决方案

virtualized List,以及antd 的组件有实现虚拟化列表的功能。

自己的虚拟化列表

虽然已有的库可以解决日常开发的问题,但如果因为项目原因不能引用第三方库呢,所以还需要化为自己的东西,所以这里就参考virtualized List的实现,手写一个简单的虚拟化列表。

原理

原理就是内外两个列表,外列表(视图列表)定高,内列表(总高列表)根据数据总数和行高算出总高度,然后滚动的时候,算出当前在内列表(总高列表)到顶部的距离,根据距离算出当前应该显示的列表的首行以及尾行,然后截取数组渲染。每行的定位为绝对定位,根据行数,算到顶部的距离。

代码

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

const LIST_HEIGHT = 640;        // 列表的高度
const SINGLE_ROW_HEIGHT = 128;  // 每行的高度
const ALL_DATA_LENGTH = 10000;  // 后端返回的列表长度
const LOAD_MORE_LINE = 2;       // 为了防止滚动出现空白,多渲染前后的行数。 
const VIEW_LINE = 5;            // 列表显示的行数

// 视图列表的样式 需要定高
const outerListStyle = {
  height: LIST_HEIGHT,
  overflow: 'auto',
  background: '#ECECEC',  
};

// 列表每一行div的样式 需要定高
const rowStyle = {
  height: SINGLE_ROW_HEIGHT,
  background: '#fff',
  border: '1px solid',
  position: 'absolute',
  width: '300px',
};

// 获取所有列表的工具函数
const getInitData = (number) => {
  const list = [];
  for (let i = 0; i < number; i += 1) {
    list.push({
      index: i,
      key: `list${i}`,
      value: `row${i}`,
    });
  }
  return list;
};

const allData = getInitData(ALL_DATA_LENGTH);

// 防抖
const debounce = (fn, delay = 200) => {
  if (typeof fn !== 'function') {
    // 参数类型为函数
    throw new TypeError('fn is not a function');
  }
  let lastFn = null;
  return function (...args) {
    if (lastFn) {
      clearTimeout(lastFn);
    }
    let lastFn = setTimeout(() => {
      lastFn = null;
      fn.call(this, ...args);
    }, delay);
  };
};

// 组件
class List extends React.Component {
  componentDidMount() {
    this.listDom = document.getElementById('outer-list');
    this.listDom.addEventListener('scroll', this.handleScroll);
  }

  componentWillUnmount() {
    this.listDom.removeEventListener('scroll', this.handleScroll);
  }

  state = {
    viewData: allData.slice(0, VIEW_LINE),
  };
  
  // 滚动回调函数
  handleScroll = debounce(() => {
    const scrollTop = this.listDom.scrollTop;
    const computeStarIndex = parseInt(scrollTop / SINGLE_ROW_HEIGHT);
    const startIndex = computeStarIndex - LOAD_MORE_LINE > 0  // 根据滚动条滚动的距离 算当前应该显示的首行行数
      ? computeStarIndex - LOAD_MORE_LINE
      : 0;
    const compotedEndINdex = (computeStarIndex > startIndex ? computeStarIndex : startIndex)
      + LOAD_MORE_LINE
      + VIEW_LINE;
    const endIndex = compotedEndINdex > ALL_DATA_LENGTH ? ALL_DATA_LENGTH : compotedEndINdex; //算加上前后多渲染行数的 末行行数
    const viewData = allData.slice(startIndex, endIndex); // 截取对应区间 生成显示数组
    this.setState({
      viewData,
    });
    console.log('startIndex', startIndex);
    console.log('endIndex', endIndex);
  }, 500);

  renderRow = (item) => {
    const computerTop = item.index * SINGLE_ROW_HEIGHT;  
    return (
      <div
        key={item.key}
        style={{
          ...rowStyle,    
          top: computerTop,  //划重点 利用当前的行数和行高算到顶部的距离
        }}
      >
        <div>{item.key}</div>
        <div>{item.value}</div>
      </div>
    );
  };

  render() {
    const { viewData } = this.state;
    return ( 
     //外层视图显示的列表
      <div id="outer-list" style={outerListStyle} >  
        <div //内层列表,需要算所有数据的高度
          id="inner-list"
          style={{
            height: SINGLE_ROW_HEIGHT * allData.length,
            position: 'relative',
          }}
        >
          {viewData.map(item => this.renderRow(item))}
        </div>
      </div>
    );
  }
}

ReactDOM.render(<List />, document.getElementById('container'));

总结

以上就是一个简单的虚拟化列表了。