手动实现虚拟列表,处理后端一次返回10万条数据

先看效果

手动实现虚拟列表,处理后端一次返回10万条数据_第1张图片

什么是虚拟列表

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为550px,而列表项的高度为55px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可

为啥需要使用虚拟列表

虚拟列表可以解决一次性渲染数据量过大时,页面卡顿,(比如: table不分页并且一次性加载上万条复杂的数据)

虚拟列表组件和样式

import React, {
    useRef,
    useEffect,
    useContext,
    createContext,
    useReducer,
    useState,
    useMemo
} from 'react'
import { throttle, noop } from 'lodash'

// ===============reducer ============== //
const initialState = {
    // 行高默认值
    rowHeight: 50,
    // 当前scrollTop
    curScrollTop: 0,
}

function reducer(state: any, action: any) {
    switch (action.type) {
        //渲染的列表tr改变
        case 'changeTrs': {
            const curScrollTop = action.curScrollTop
            return {
                ...state,
                curScrollTop,
            }
        }
        default:
            throw new Error()
    }
}

// ===============context ============== //
const ScrollContext = createContext({
    dispatch: noop,
    renderLen: 1,
    start: 0,
    rowHeightMap: {},
    rowHeightSum: [],
    rowHeight: initialState.rowHeight,
})
// =============组件 =================== //
function VWrapper(props: any): JSX.Element {
    const { children, ...restProps } = props

    const { renderLen, start, rowHeightMap, rowHeightSum } = useContext(ScrollContext)
    console.log(rowHeightMap, rowHeightSum, renderLen, start)
    const contents = children[1]

    const tempNode = useMemo(() => {
        if (Array.isArray(contents) && contents.length) {
            return [children[0], contents.slice(start, start + renderLen)]
        }
        return children
    }, [children, contents, renderLen, start])

    return <tbody {...restProps}>{tempNode}</tbody>
}

function VRow(props: any, ref: any): JSX.Element {
    const { children, style, ...restProps } = props
    return (
        <tr
            {...restProps}
            style={{
                ...style,
                boxSizing: 'border-box'
            }}
        >
            {children}
        </tr>
    )
}

function VCell(props: any): JSX.Element {
    const { children, ...restProps } = props
    return <td {...restProps}>{children}</td>
}
function VTable(props: any, otherParams: any): JSX.Element {
    const { style, children, ...rest } = props
    const { width, ...rest_style } = style

    const { rowKeyList } = otherParams ?? {}

    const [state, dispatch] = useReducer(reducer, initialState)

    //行索引与行高对应的map初始值
    const originData: any = {}

    rowKeyList?.map((cv: string, index: number) => {
        originData[index] = state.rowHeight
        return cv
    })

    //记录行高累加高度
    const rowHeightSum: number[] = []
    //行索引与行高对应的map
    const [rowHeightMap, setRowHeightMap] = useState(originData)

    let currowHeightSum = 0
    rowKeyList.map((cv: string, index: number) => {
        currowHeightSum += rowHeightMap[index]
        rowHeightSum.push(currowHeightSum)
        return cv
    })

    //二分查找:第一个累加高度大于scrollTop的数据索引
    const binarySearch = (array: number[], value: number) => {
        let left = 0
        let right = array.length - 1
        while (left <= right) {
            const middle = left + Math.floor((right - left) / 2)
            if (array[middle] >= value) right = middle - 1
            else left = middle + 1
        }
        return left
    }

    // 渲染的第一条数据的索引
    let start = 0
    start = binarySearch(rowHeightSum, state.curScrollTop)

    //初始渲染条数
    const [renderLen, setDefaultRenderLen] = useState(0)

    const wrap_tableRef = useRef<HTMLDivElement>(null)
    const tableRef = useRef<HTMLTableElement>(null)

    //计算当前屏幕能展示数据条数
    useEffect(() => {
        const parentHeight = (wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight
        setDefaultRenderLen(Math.ceil(parentHeight / state.rowHeight))
    }, [state.rowHeight])

    // 偏移量,向上滚动的动画效果
    let offsetStart = rowHeightSum[start]
        ? rowHeightMap[start] - (rowHeightSum[start] - state.curScrollTop)
        : 0

    const result = useMemo(() => rowKeyList.length - renderLen - start, [
        renderLen,
        rowKeyList.length,
        start
    ])
    //滚动到最底部时,偏移量重置为0
    if (result <= 0) {
        offsetStart = 0
    }

    // virtuallist总高度
    const tableHeight = rowHeightSum[rowKeyList.length - 1] || state.rowHeight * rowKeyList.length

    useEffect(() => {
        const curRowHeightMap: any = {}
        let n = 1
        while (n <= renderLen) {
            const index = rowKeyList.indexOf(
                tableRef.current?.getElementsByTagName('tr')[n]?.getAttribute('data-row-key')
            )
            if (index > -1) {
                curRowHeightMap[index] = tableRef.current?.getElementsByTagName('tr')[n]?.offsetHeight
            }
            n += 1
        }

        setRowHeightMap((prevState: any) => {
            return Object.assign(prevState, curRowHeightMap)
        })
    }, [renderLen, state.curScrollTop, rowKeyList])

    useEffect(() => {
        const throttleScroll = throttle(e => {
            const scrollTop: number = e?.target?.scrollTop ?? 0
            dispatch({
                type: 'changeTrs',
                curScrollTop: scrollTop,
            })
        }, 16)

        const ref = wrap_tableRef?.current?.parentNode as HTMLElement

        if (ref) {
            ref.addEventListener('scroll', throttleScroll, {
                //防止调用事件监听器:为了提高滚动性能
                passive: true,
            })
        }

        return () => {
            ref.removeEventListener('scroll', throttleScroll)
        }
    }, [wrap_tableRef])

    return (
        <div
            className="virtuallist"
            ref={wrap_tableRef}
            style={{
                width: '100%',
                position: 'relative',
                height: tableHeight,
                boxSizing: 'border-box'
            }}
        >
            <ScrollContext.Provider
                value={{
                    dispatch,
                    rowHeight: state.rowHeight,
                    start,
                    rowHeightMap,
                    rowHeightSum,
                    renderLen,
                }}
            >
                <table
                    {...rest}
                    ref={tableRef}
                    style={{
                        ...rest_style,
                        width,
                        position: 'sticky',
                        top: 0,
                        transform: `translateY(-${offsetStart}px)`
                    }}
                >
                    {children}
                </table>
            </ScrollContext.Provider>
        </div>
    )
}

const transformTable = (tempObj: any) => {
    return (props: any) => VTable(props, tempObj)
}

// ================导出===================
export function VList(props: { rowKeyList?: string[] }): any {
    const TableComponent = transformTable({
        rowKeyList: props.rowKeyList,
    })
    return {
        table: TableComponent,
        body: {
            wrapper: VWrapper,
            row: VRow,
            cell: VCell
        }
    }
}



.virtuallist .ant-table-tbody > tr > td > div {
    box-sizing: border-box;
    white-space: nowrap;
    vertical-align: middle;
    overflow: hidden;
    text-overflow: ellipsis;
    width: 100%;
}

.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
    overflow: inherit;
}

.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
    border-right: 1px solid #f0f0f0;
}

使用方式

import React, { useMemo } from "react";
import { Table, Tag } from "antd";
import { VList, } from "../../src/index";
import 'antd/dist/antd.css';

const generateData = () => {
  let tempDataSource = [];
  for (let i = 0; i < 100000; i++) {
    tempDataSource.push({
      uuid: `${i}`,
      company_name1: i % 3 !== 1 ? `${i} 翠微股份给点力` : <> <Tag >翠微股份给点力翠微股份给点力</Tag><Tag >翠微股份给点力翠微股份给点力</Tag><Tag >翠微股份给点力翠微股份给点力</Tag></>,
      company_name2: `${i} company index`,
      company_name3: `${i} company company`,
      company_name4: `${i} company company`,
    });

  }

  return tempDataSource;
};

function SinglePageLoading() {
  const dataSource = generateData()

  const loading = false

  const columns: any = [
    {
      title: '序号',
      dataIndex: "uuid",
      fixed: 'left',
      width: 100,
    },
    {
      title: "股1",
      dataIndex: "company_name1",
      width: 200
    },
    {
      title: "股2",
      dataIndex: "company_name2",
      width: 200
    },
    {
      title: "股3",
      dataIndex: "company_name3",
      width: 200
    }
  ];



  const components = useMemo(() => {
    return VList({
      rowKeyList: dataSource.map(cv => cv.uuid)
    })
  }, [dataSource])

  return (
    <>
      <Table
        columns={columns}
        dataSource={dataSource}
        pagination={false}
        loading={loading}
        scroll={{ y: 500, x: '100%' }}
        rowKey={"uuid"}
        components={components}
      />
    </>
  );
}

export default SinglePageLoading

ok,完事~

你可能感兴趣的:(react.js,javascript,typescript,虚拟列表)