100行代码带你实现react虚拟列表渲染-bysking

109 阅读2分钟

摘要

基于react实现的固定高度的虚拟列表,使用相对较小的代码量,聚焦核心逻辑,方便小白学习了解原理

一、效果

image.png

二、代码

import { throttle } from 'lodash';
import React, { useMemo, useState } from 'react';

interface typeProps {
  /** 全部的数据 */
  list: { name: string; key: string }[];
}
const HEIGHT = 100; // 每一行元素的渲染高度,先讨论固定高度,动态高度后续另外开文章
const PAGE_COUNT = 10; // 每一页的渲染条数, 灵活一点的话,可以结合可视区高度除以HEIGHT动态计算

const Vl = (props: typeProps) => {
  const alllist = props.list || [];

  /** 跟踪滚动高度 */
  const [scrollTop, setScrollTop] = useState(0);

  /** 关注scrollTop的变化,然后计算切片开始的位置 */
  const startIndex = useMemo(() => {
    /** 当前滚动高度除以每个元素的高度,向下取整 */
    let index = Math.floor(scrollTop / HEIGHT);

    // // 满足条件下,往前多渲染两个,解决留白问题
    if (index > 10) {
      return index - 3;
    }

    return index;
  }, [scrollTop]);

  /** 结束位置直接根据开始位置加上PAGE_COUNT计算,再处理一下数据越界 */
  const endIndex = useMemo(() => {
    if (startIndex + PAGE_COUNT >= alllist.length) {
      return alllist.length;
    } else {
      return startIndex + PAGE_COUNT + 1;
    }
  }, [startIndex, alllist.length]);

  /** 开始和结束位置都计算好了,准备对原始数据进行切片 */
  const renderList = alllist.slice(startIndex, endIndex);

  /** 最后,需要注意,1000个元素和切片出来的10个元素能撑开的高度是不一样的,需要再计算一下上下的差距,用padding撑开滚动区域 */
  const paddingTop = startIndex > 0 ? startIndex * HEIGHT : 0;
  const paddingBottom =
    endIndex < alllist.length - 1
      ? (alllist.length - endIndex - 1) * HEIGHT
      : 0;

  /** 监听处理滚动事件,记得加个节流 */
  const onScroll = throttle(
    (e) => {
      setScrollTop(e.target.scrollTop);
    },
    200,
    {
      leading: true,
      trailing: false,
    },
  );

  return (
    <div>
      <div>
        {/* 变量观测 */}
        {startIndex} - {endIndex} - 总计: {alllist.length}, 滚动高度:{' '}
        {scrollTop}
      </div>

      <div
        style={{ border: '1px solid red', height: '400px', overflowY: 'auto' }}
        onScroll={onScroll}
      >
        {/* 用于撑开上面滚动条空间 */}
        <div style={{ paddingTop: paddingTop + 'px' }}></div>

        {/* 切片渲染部分 */}
        {renderList.map((item) => {
          return (
            <div
              style={{ height: '100px', margin: '2px', background: '#eee' }}
              key={item.key}
            >
              <div>{item.name}</div>
            </div>
          );
        })}

        {/* 用于撑开下面滚动条空间 */}
        <div style={{ paddingBottom: paddingBottom + 'px' }}></div>
      </div>
    </div>
  );
};

export default React.memo(Vl);

三、测试

import Vl from './vl';

const VlistTest = () => {
  const list = Array(1000)
    .fill(1)
    .map((item, index) => {
      return {
        name: 'name' + index,
        key: 'key_' + index,
      };
    });

  return <Vl list={list} />;
};

export default VlistTest;