React虚拟列表|青训营笔记

177 阅读2分钟

React虚拟列表|青训营笔记

这是我参与「第四届青训营 」笔记创作活动的的第10天

一、前言

在开发项目中,可能会遇到这样一个需求,需要加载一个长列表,数据有很多条,上万甚至上千条,这时候我们总不能 把所有的数据都挂载到dom上去把,这样页面的性能会降低到极限

二、思路

因此,可以使用一个虚拟列表来代替真实的列表,也就是说,只要挂载可视区域的数据就行了,其它不可见的地方就不挂载

三、具体实现

1. 高度准备

  • 定义滚动区域的屏幕高度
  • 定义所有列表的高度(总列表的高度,用于撑开容器经行滚动)
  • 定义包裹内容的高度,用来跟随上下位移

2. 滚动值的计算

  • 计算需要加载的列表数: 1+Math.ceil(屏幕高度/ 元素列表高度) 多加载一项是为了防抖
  • 开始值: Math.floor(scrollTop / 元素列表高度) 屏幕高度/单一元素高度 得到滚动了多少元素
  • 结束值: 开始值+列表数
  • transformY: 下拉的位移 根据滚动过多少元素,得到向下的位移 (开始值*每个元素的高度)

3. 实现思路

  • onScroll 滚动事件 监听 scrollTop的值 从而改变start(开始位置)的值,使用useState()
  • end 位置 通过监听 start、limit、list 的值是否改变,是用 useMemo()重置
  • renderList 对列表数组经行截取 只渲染 start--end 部分
  • renderList 里需要有传入过来的 jsx,用于渲染

4.代码部分

  1. ReactVirtualList.jsx
import React, { useState, useMemo, memo } from 'react'
import './css.less'
import { useCallback } from 'react';
import { useRef } from 'react';

const ReactVirtualList = memo((props) => {
  let { list, item: Item, contentWidth, contentHeight, itemHeight } = props

    const [start, setStart] = useState(0)

    const listDom = useRef()

    // 显示的最大条数
    const limit = useMemo(() => {
        return 1 + Math.ceil(contentHeight / (itemHeight))
    }, [contentHeight, itemHeight]);

    // 滚动事件
    const scrollHandler = useCallback((e) => {
            const top = e.target.scrollTop
            const curStart = Math.floor(top / (itemHeight))
            curStart !== start && setStart(curStart)
        },[itemHeight, start])

    // 结束位置
    const end = useMemo(() => {
        return Math.min(start + limit, list.length)
    }, [start, limit, list]);

    const renderList = useMemo(() => {
        return list
            .slice(start, end)
            .map((v, i) => (
                <span key={v.id} id={v.id} itemHeight={itemHeight} >
                    <Item value={v.v}></Item>
                </span>
            ))
    }, [start, end, list, itemHeight]);

    const transformY = useMemo(() => {
        return start * itemHeight + 'px'
    }, [start, itemHeight]);

    return (
      <div className='island-virtual-list' 
           ref={listDom} 
           onScroll={(e) => scrollHandler(e)} 
           style={{ width: contentWidth + 'px', height: contentHeight + 'px' }}
      >
        <div className="listWrapper" 
             style={{ height: itemHeight * list.length + 'px' }}
        >
            <div className="itemWrapper" 
                 style={{ height: contentHeight + 'px', transform: `translate3d(0, ${transformY}, 0)` }}
            >
                {renderList}
            </div>
        </div>
    </div>
    )
     
})

export default ReactVirtualList

  1. css.less
.island-virtual-list {
  position: absolute;
  margin: auto;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  opacity: 0.8;
  overflow-y: auto;
  overflow-x: hidden;
  border: 1px solid #000;
}

.listWrapper {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.itemWrapper {
  transform: translate3d(0, 0, 0);
}

  1. App.jsx
import React, { memo } from 'react'
import ReactVirtualList from './ReactVirtualList';

// 定义可是滚动的样式
const styleObj = {
  contentWidth: 800,
  contentHeight: 300,
  itemHeight: 10,
  itemWidth: 60
}

// 被传入的jsx, 最终渲染的每一条的 样式
const Item = (props) => {
  return <div className='list-item'>{props.value}</div>
}

// 数据列表
const list = Array(Math.floor((Math.random() + 1) * 10000)).fill().map((v, i, arr) => ({ id: i, v: i + '/' + arr.length + '   行' }))


const App = memo(() => {
  return <div className='app'>
    <ReactVirtualList {...styleObj} list={list} item={Item} ></ReactVirtualList>
  </div>
})

export default App
  1. 效果

demo.gif

5. 在具体项目中使用

index.jsx

import React, { memo } from 'react'

import ReactVirtualList from '../../../../../components/ReactVirtualList'
import calculateTimeLength from '../../../../../utils/calculateTimeLength'
import { MioSonglistBottomSonglistDiv } from './css'

// 定义可是滚动的样式
const styleObj = {
  contentHeight: 500,
  itemHeight: 35,
}

// 渲染的一条数据 
const songItem =(props) => {
  return (
    <div className='song-list-item'>
        <span className="act">
          <span className="number">{props.value.index<10?`0${props.value.index}`:props.value.index}</span>
          <span className='icon'></span>
        </span>
        <span className="title">{props.value.item.name}</span>
        <span className="singer">
          {
            props.value.item.ar.map((item,index) => {
              return (
                <span key={item.id}>
                  <span>{item.name}</span>
                  {index+1 !=props.value.item.ar.length && <span>/</span>}
                </span>
              )
            })
          }
        </span>
        <span className="album">{props.value.item.al.name}</span>
        <span className="time">{calculateTimeLength(props.value.item.dt)}</span>
      </div>
  )
}
// 操作 标题 歌手 专辑 时间

const MioSonglistBottomSonglist = memo((props) => {

  const {songlist} = props;
  console.log(songlist[0]);
  return (
    <MioSonglistBottomSonglistDiv onScroll={e=>{console.log(e.target.scrollTop)}}>
      <div className="song-list-item-top">
        <span className="act top">操作</span>
        <span className="title top">歌曲</span>
        <span className="singer top">歌手</span>
        <span className="album top">专辑</span>
        <span className="time top">时间</span>
      </div>
    
    <ReactVirtualList {...styleObj} list={songlist} item={songItem}/>
    {
      songlist.length<=10 && <div>loading</div>
    }
    </MioSonglistBottomSonglistDiv>
  )
})

export default MioSonglistBottomSonglist

tips

因为是放在组件中,因此宽度是依赖父组件自适应的,所以使用时不用再设置宽度。

效果如下

demo2.gif

效果还是很不错的