长列表渲染(react-virtualized)

1 概念 

问题:DOM元素过多会导致页面卡顿。

原因:DOM过多会占用过多的内存;频繁操作DOM时触发重绘重排,消耗浏览器性能。

三种解决办法

  • 数据分页

        较为常用(缺点是可能一次性渲染过多)

  • 无限滚动

        渲染的部分即将滚动完,再渲染下面的部分(缺点是老数据堆积)

  • 虚拟滚动

        可视区域渲染,会预先加载一部分数据,避免出现白屏

                ​​​​​​​        ​​​​​​​        ​​​​​​ ​​​​​​​e6b2830c52aaa871043436ea42ca899f.png     

2 编码实现

import React, { useEffect, useState, useRef } from 'react';
import { NavBar, Toast } from 'antd-mobile'
import './index.scss'
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { getCurrentCity } from '../../utils';
import { List, AutoSizer } from 'react-virtualized';

export default function CityList () {
  const navigate = useNavigate()
  const cityListComponent = useRef(null);
  const [citylist, setCitylist] = useState({})
  const [cityIndex, setCityIndex] = useState([])
  const [activeIndex, setActiveIndex] = useState(0)
  const HOUSE_CITY = ['北京', '上海', '广州', '深圳']

  useEffect(() => {
    async function getCity () {
      await getCityLisy();
      try {
        cityListComponent.current.measureAllRows()
      } catch (err) {
        // console.log(err, 'err')
      }
    }
    getCity();
  }, [])

  const TITLE_HEIGHT = 36
  const NAME_HEIGHT = 50

  // 分类数据格式化
  const formatData = list => {
    const citylist = {}
    list.forEach(item => {
      const first = item.short.substr(0, 1);
      if (citylist[first]) {//判断是否存在字母缩写
        citylist[first].push(item)
      } else {
        citylist[first] = [item]
      }
    })
    const cityIndex = Object.keys(citylist).sort()
    return {
      citylist, cityIndex
    }
  }
  //  字母索引格式化
  const formatCityIndex = letter => {
    switch (letter) {
      case '#':
        return '当前定位'
      case 'hot':
        return '热门城市'
      default:
        return letter.toUpperCase()
    }
  }

  // 列表数据:三部分
  const getCityLisy = async () => {
    // 数据1
    const res = await axios.get('http://localhost:8080/area/city?level=1')
    const { citylist, cityIndex } = formatData(res.data.body)
    // 数据2
    const hotRes = await axios.get('http://localhost:8080/area/hot')
    citylist['hot'] = hotRes.data.body
    cityIndex.unshift('hot')
    // 数据3
    const curCity = await getCurrentCity()
    citylist['#'] = [curCity]
    cityIndex.unshift('#')
    setCitylist(citylist)
    setCityIndex(cityIndex)
    console.log('diaoyongjiekou')
    // console.log('城市列表数据:', citylist, cityIndex, curCity)
  }

  // 切换城市
  const changeCity = ({ label, value }) => {
    if (HOUSE_CITY.indexOf(label) > -1) {
      localStorage.setItem('hkzf_city', JSON.stringify({ label, value }))
      navigate(-1)
    } else {
      Toast.show({
        content: '该城市暂无房源数据',
      })
    }
  }
  // 渲染行
  const rowRenderer = ({
    key, // Unique key within array of rows
    index, // Index of row within collection
    isScrolling, // The List is currently being scrolled
    isVisible, // This row is visible within the List (eg it is not an overscanned row)
    style, // Style object to be applied to row (to position it)
  }) => {
    const letter = cityIndex[index]
    return (
      <div key={key} style={style} className="city">
        <div className="title">{formatCityIndex(letter)}</div>
        {citylist[letter].map(item => (
          <div className="name" key={item.value} onClick={() => { changeCity(item) }}>
            {item.label}
          </div>
        ))}
      </div>
    );
  }

  // 行高度
  const getRowHeight = ({ index }) => {
    // 标题高度+子项目个数*子项目高度
    return TITLE_HEIGHT + citylist[cityIndex[index]].length * NAME_HEIGHT
  }

  // 左侧联动右侧:获取startIndex,更新右侧activeIndex
  const rowsRendered = ({ startIndex }) => {
    // console.log(startIndex, 'startIndex右')
    if (activeIndex !== startIndex) {
      setActiveIndex(startIndex)
    }
  }

  // 右侧联动左侧:ref绑定List组件,调用实例方法
  // 问题:渲染过的列表才能精确跳转,提前计算每一行高度来解决
  const renderCityIndex = () => {
    return cityIndex.map((item, index) => (
      <li className="cityItem" key={item} onClick={() => { cityListComponent.current.scrollToRow(index); console.log(index, '左'); setActiveIndex(index) }}>
        <span className={activeIndex === index ? 'index-active' : ''}>
          {item === 'hot' ? '热' : item.toUpperCase()}
        </span>
      </li>
    ))
  }

  return (
    <>
      <div className='citylist'>
        <NavBar className="navbar" onBack={() => { navigate(-1) }}>城市选择</NavBar>
        <AutoSizer>
          {({ height, width }) => (//需要设置根元素高度
            <List
              ref={cityListComponent}
              width={width}
              height={height}
              rowCount={cityIndex.length}
              rowHeight={getRowHeight}
              rowRenderer={rowRenderer}
              onRowsRendered={rowsRendered}
              scrollToAlignment="start"
            />
          )}
        </AutoSizer>
        <ul className="city-index">{renderCityIndex()}</ul>
      </div>
    </>
  );
};

3 编码分析:

  • 数据处理

  •  组件渲染

        可视区域的宽高由高阶组件AutoSizer动态计算;

        List组件渲染需要可视区域宽高、数组长度、子项高度;

        左侧联动右侧:更新右侧activeIndex;

        右侧联动左侧:ref绑定List组件,借用组件的实例方法精确跳转(提前计算每一行高度)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值