前端虚拟列表深度拆解

虚拟列表是为了解决什么问题

真实项目中的痛点:

想象一个后台系统:用户列表:10 万条;订单列表:20 万条;日志列表:百万级;表格里还有:多列、复杂 DOM、hover、操作按钮、状态标签

直接 map 渲染:

data.map(item => <Row key={item.id} />)

会遇到:首次渲染卡死、滚动严重掉帧、内存暴涨和浏览器直接崩

根因只有一个:DOM 太多浏览器不是怕 JS,浏览器最怕的是成千上万个 DOM 节点

总的来说虚拟列表就是只渲染可视区域内的列表项,而其余的用占位高度“假装存在”

虚拟列表的核心思想

我总结主要要理解这四点:
1.可视区域(viewport):屏幕当前能看到的那一段高度

2.列表总高度(total height):假设所有 item 都渲染后的总高度(假的,但要算出来)

3.起始索引和结束索引:根据滚动距离,计算现在应该显示哪几条

4. 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置

虚拟列表的本质实现原理

假设每一项高度固定,这个是最简单的,真是项目中大量列表数据一般都是每一项数据都是高度固定的

itemHeight = 50px 容器高度 = 500px

那屏幕最多能显示:500 / 50 = 10 条

通常会多渲染几条(缓冲区):实际渲染 = 10 + 4 = 14 条

根据滚动距离算索引

startIndex = Math.floor(scrollTop / itemHeight) endIndex = startIndex + visibleCount

不渲染所有 DOM,但要让滚动条是对的

<div> <div></div> <!-- 撑开高度 --> <div></div> <!-- 只放可见项 --> </div>
.phantom { height: totalCount * itemHeight; }

偏移当前渲染区域

offsetY = startIndex * itemHeight
.list { transform: translateY(offsetY); }

视觉效果:DOM 只有十几条、滚动条像真的有十万条

真实项目React中使用demo整体代码(可直接使用)

import React, { useRef, useState, useEffect, useMemo,useCallback } from 'react' interface VirtualListProps<T> { data: T[] height: number // 容器高度 itemHeight: number // 每一项高度(固定) renderItem: (item: T, index: number) => React.ReactNode buffer?: number // 缓冲区 } export function VirtualList<T>({ data, height, itemHeight, renderItem, buffer = 5, }: VirtualListProps<T>) { const containerRef = useRef<HTMLDivElement>(null) const [scrollTop, setScrollTop] = useState(0) /** 可视区能显示的条数 */ const visibleCount = Math.ceil(height / itemHeight) /** 开始索引 */ const startIndex = Math.max( Math.floor(scrollTop / itemHeight) - buffer, 0 ) /** 结束索引 */ const endIndex = Math.min( startIndex + visibleCount + buffer * 2, data.length ) /** 当前渲染的数据 */ const visibleData = useMemo( () => data.slice(startIndex, endIndex), [data, startIndex, endIndex] ) /** 偏移量 */ const offsetY = startIndex * itemHeight /** 总高度(关键:撑开滚动条) */ const totalHeight = data.length * itemHeight /** 滚动事件 */ const onScroll = useCallback(() => { if (!containerRef.current) return setScrollTop(containerRef.current.scrollTop) }, []) return ( <div ref={containerRef} style={{ height, overflowY: 'auto', position: 'relative', border: '1px solid #ddd', }} onScroll={onScroll} > {/* 撑开高度 */} <div style={{ height: totalHeight }} /> {/* 实际渲染内容 */} <div style={{ position: 'absolute', top: 0, left: 0, right: 0, transform: `translateY(${offsetY}px)`, }} > {visibleData.map((item, index) => ( <div key={startIndex + index} style={{ height: itemHeight, boxSizing: 'border-box', borderBottom: '1px solid #eee', }} > {renderItem(item, startIndex + index)} </div> ))} </div> </div> ) }

如何使用这个 VirtualList

const data = Array.from({ length: 100000 }, (_, i) => `Row ${i}`) export default function App() { return ( <VirtualList data={data} height={500} itemHeight={50} renderItem={(item) => <div>{item}</div>} /> ) }

针对这个demo,我总结出几个关键点

1.下面这行代码,不显示任何内容,只是为了撑开滚动条

<div style={{ height: totalHeight }} />

2.为什么 content 要 absolute + translateY?因为transform不会出发布局重排,性能比设置top好

transform: translateY(offsetY)

3.buffer 的意义,是防止滚动过快出现白屏,提前渲染上下几条

buffer = 5

4.为什么 key 用startIndex + index因为:同一条数据在不同 scrollTop 下会复用 DOM,key 必须全局唯一

可以优化的地方:可以对scroll进行一个节流处理

用 ref 记录 RAF 状态

const rafIdRef = useRef<number | null>(null)

改变 onScroll函数

const onScroll = useCallback(() => { if (!containerRef.current) return // 如果当前帧已经有任务了,直接 return if (rafIdRef.current !== null) return rafIdRef.current = requestAnimationFrame(() => { setScrollTop(containerRef.current!.scrollTop) rafIdRef.current = null }) }, [])

Read more

算法王冠上的明珠——动态规划之路径问题(第一篇)

算法王冠上的明珠——动态规划之路径问题(第一篇)

目录 1. 什么叫路径类动态规划 一、核心定义(通俗理解) 二、核心特征(识别这类问题的关键) 2. 动态规划步骤 状态表示 状态转移方程 初始化 填表顺序 返回值 3. 例题讲解 3.1 LeetCode62. 不同路径 3.2 LeetCode63. 不同路径 II 3.3 LeetCodeLCR 166. 珠宝的最高价值 今天我们来聊一聊动态规划的路径类问题。 1. 什么叫路径类动态规划 路径类动态规划是 动态规划的一个重要分支,核心解决 “从起点到终点的路径相关问题”—— 比如 “路径总数”“最短路径长度”“路径上的最大 / 最小和” 等,其本质是通过 “状态递推” 避免重复计算,高效求解多阶段决策的路径问题。 一、

By Ne0inhk
【动态规划】数位DP的原理、模板(封装类)

【动态规划】数位DP的原理、模板(封装类)

本文涉及知识点 C++动态规划 复杂但相对容易理解的解法 上界、下界的位数一样都为N。如果不一样,拆分一样。比如:[10,200],拆分[10,99]和[100,200]。由于要枚举到 1 ∼ N 1\sim N 1∼N,故实际复杂度是N倍。 动态规划的状态表示 dp[n][m][m1],n表示已经处理最高n位,m表示上下界状态:0非上下界,1下界,2上界,3上下界。m1是自定义状态。 某题范围是[110,190],处理一位后:1是上下界,无其它合法状态。处理二位后,11是下界,19是上界, 12 ∼ 18 12

By Ne0inhk
【LeetCode_27】移除元素

【LeetCode_27】移除元素

刷爆LeetCode系列 * LeetCode27题: * github地址 * 前言 * 题目描述 * 题目思路分析 * 代码实现 * 算法代码优化 LeetCode27题: github地址 有梦想的电信狗 前言 本文用C++实现LeetCode 第27题 题目描述 题目链接:https://leetcode.cn/problems/remove-element/ 题目思路分析 目标分析: 1. 将数组中等于val的元素移除 2. 原地移除,意味着时间复杂度为O(n),空间复杂度为O(1) 3. 返回nums中与val值不同的元素个数 思路:双指针 * src:用于扫描元素,从待扫描元素的第一个开始,因此初始下标为0 * dst:指向数组中,最后一个位置正确的元素的下标,因此初始值为-1 * count:记录赋值的次数,赋值的次数即为数组中与val值不同的元素个数,初始值为0 操作: * nums[

By Ne0inhk
一文彻底搞清楚数据结构之快速排序和归并排序的深入优化

一文彻底搞清楚数据结构之快速排序和归并排序的深入优化

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》《数据结构与算法初阶》 ✨逆境不吐心中苦,顺境不忘来时路!🎬 博主简介: 前言:前面小编已经介绍八大排序算法的基本思想和实现方法!但关于其中的快速排序和归并排序还有一些细节可以优化!接下来跟着小编来看看快速排序和归并排序的深入优化,学习一下优化完之后,具体在实际中的应用!废话不多说,下面跟着小编的节奏🎵一起学习吧! 目录 * 1.快速排序性能的关键点分析 * 1.1三路划分算法思想讲解 * 1.2hoare和lomuto和三路划分单趟排序代码分析 * 1.3三种快排单趟排序运⾏结果分析 * 2.排序数组OJ题 * 2.1lomuto的快速排序跑排序数组OJ题 * 2.2hoare的快速排序跑排序数组OJ题 * 2.3三路划分的快速排序跑排序数组OJ题 * 2.4introsort的快速排序跑排序数组OJ题 * 3.外排序介绍 * 3.1创建随机数据⽂件的代码 * 3.2⽂件归并排序思路分析 * 3.3⽂件归并排序代码实现 * 3.4非递归版

By Ne0inhk