虚拟列表的渐进式实现,vue,react

1、地址(vue)

2、react (下面是几个版本 样式在最后面)

/*
 * @Author: your name
 * @Date: 2020-03-13 11:11:23
 * @LastEditTime: 2020-05-13 16:39:01
 * @LastEditors: Please set LastEditors
 * @Description: 列表项等宽
 * @FilePath: /optimus/src/pages/test/Index/hooks.js
 */
import React, { useState, useEffect, useRef } from "react";
import styles from "./style.less";

const arr = [];
for (let index = 0; index < 200; index++) {
  arr.push({
    index,
    height: parseInt(Math.random() * 30 + 20)
  });
}

const Index = (props) => {
  const [height, setHeight] = useState(0);
  const [itemHeight] = useState(30);

  const [totalList] = useState(arr);
  const [list, setList] = useState([]);

  const couterRef = useRef();
  const totalRef = useRef();

  useEffect(() => {
    setHeight(totalList.length * itemHeight);
  }, [totalList]);

  useEffect(() => {
    updateVisibleData();
  }, [totalList]);

  const onScrollCapture = (view) => {
    const scrollTop = totalRef.current.scrollTop;
    updateVisibleData(scrollTop);
  };

  const updateVisibleData = (scrollTop) => {
    scrollTop = scrollTop || 0;

    const clientHeight = totalRef.current.clientHeight;
    const visibleCount = Math.ceil(clientHeight / itemHeight);
    const start = Math.floor(scrollTop/ itemHeight);
    const end = start + visibleCount;

    const _list = totalList.slice(start, end);
    setList(_list);

    couterRef.current.style.webkitTransform = `translate3d(0, ${start * itemHeight}px, 0)`;
  };

  return (
    <div className={styles.wrap}>
      <div
        className={styles.listView}
        ref={totalRef}
        onScrollCapture={onScrollCapture}
      >
        <div
          className={styles.listViewPhantom}
          style={{ height: height }}
        ></div>

        <div className={styles.listViewContent} ref={couterRef}>
          {list.map((item, index) => {
            return (
              <div
                className={styles.listViewItem}
                style={{ height: itemHeight }}
                key={index}
              >
                {item.index}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
};

export default Index;
/*
 * @Author: x
 * @Date: 2020-03-13 11:11:23
 * @LastEditTime: 2020-05-13 16:34:47
 * @LastEditors: Please set LastEditors
 * @Description: react 列表项不等宽
 * @FilePath: /optimus/src/pages/test/Index/hooks.js
 */
import React, { useState, useEffect, useRef } from "react";
import styles from "./style.less";

const arr = [];
for (let index = 0; index < 200; index++) {
  arr.push({
    index,
    height: parseInt(Math.random() * 30 + 20)
  });
}

const Index = (props) => {
  const [height, setHeight] = useState(0);

  const [totalList, setTotalList] = useState(arr);
  const [list, setList] = useState([]);

  const couterRef = useRef();
  const totalRef = useRef();

  useEffect(() => {
    let total = 0;
    let index = 0;
    let length = totalList.length;
    for (index; index < length; index++) {
      total += totalList[index].height;
    }
    setHeight(total);
  }, [totalList]);

  useEffect(() => {
    updateVisibleData();
  }, [totalList]);

  const onScrollCapture = (view) => {
    const scrollTop = totalRef.current.scrollTop;
    updateVisibleData(scrollTop);
  };

  const updateVisibleData = (scrollTop) => {
    scrollTop = scrollTop || 0;

    const start = findNearestItemIndex(scrollTop);
    const end = findNearestItemIndex(scrollTop + totalRef.current.clientHeight);

    const _list = totalList.slice(start, Math.min(end + 1, totalList.length));

    setList(_list);

    couterRef.current.style.webkitTransform = `translate3d(0, ${getItemSizeAndOffset(start).offset}px, 0)`;
  };

  const findNearestItemIndex = scrollTop => {
    let total = 0;
    for (let i = 0, j = totalList.length; i < j; i++) {
      const size = totalList[i].height;
      total += size;
      if (total >= scrollTop || i === j -1) {
        return i;
      }
    }
    return 0;
  }

  const getItemSizeAndOffset = start => {
    let total = 0;
    for (let i = 0, j = Math.min(start, totalList.length - 1); i <= j; i++) {
      const size = totalList[i].height;
   
      if (i === j) {
        return {
          offset: total,
          size
        };
      }
      total += size;
    }

    return {
      offset: 0,
      size: 0
    };
  }
  

  return (
    <div className={styles.wrap}>
      <div
        className={styles.listView}
        ref={totalRef}
        onScrollCapture={onScrollCapture}
      >
        <div
          className={styles.listViewPhantom}
          style={{ height: height }}
        ></div>

        <div className={styles.listViewContent} ref={couterRef}>
          {list.map((item, index) => {
            return (
              <div
                className={styles.listViewItem}
                style={{ height: item.height }}
                key={index}
              >
                {item.index}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
};

export default Index;
.wrap{
  width: 100%;
  height: 800px;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: antiquewhite;
}

.listView {
  height: 400px;
  width: 160px;
  overflow: auto;
  position: relative;
  border: 1px solid #aaa;
}

.listViewPhantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: 100;
  width: 160px;
  background-color: rgba(red,0.5);
}

.listViewContent {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}
 
.listViewItem {
  padding: 5px;
  color: #666;
  box-sizing: border-box;
  border-bottom: 1px solid red;
  display: flex;
  align-items: center;
}

写在最后

  题目当然还可以再优化:

    对itemHeight的缓存;

    对contextHeight的高度计算;

    对缓存结果的算法查询;

    对未缓存结果的算法查询;

    根据渲染结果动态更新列表项的高度;

    数据源更新时尽量范围小的删除失效缓存;  

    。。。

  优化之路永无尽头;