花三个小时,完全掌握分片渲染和虚拟列表~大家好,我是小杜杜,有关高性能,大数据量的列表渲染的示例已经非常常见,可以说是前
大家好,我是小杜杜,有关高性能,大数据量的列表渲染的示例已经非常常见,可以说是前端必须要了解的功能点,今天我们一起手写一下,看看如何去更好的实现~
我们知道有些场景下,接口会返回出大量的数据,渲染这种列表叫做长列表,今天主要说下处理长列表的两种方式:分片渲染和虚拟列表,请各位小伙伴多多支持~
在正式开始前,希望各位小伙伴牢牢记住:js执行永远要比dom快的多,所以对于执行大量的数据,一次性渲染,非常容易造成卡顿、卡死的情况
疑问点
我们先来看看以下代码:
import React,{ useState } from 'react'; import { Button } from 'antd-mobile'; import img from './img.jpeg' // 子组件 const Item:React.FC<{id: number, waitRender?: () => void}> = ({id, waitRender}) => { return ( <div style={{display: 'flex', alignItems: 'center', marginBottom: 5}}> <img src={img} width={80} height={60} alt="" />列表{id} </div> ) } const Index:React.FC<any> = (props)=> { const [flag, setFalag] = useState<boolean>(false) const [list, setList] = useState<Array<number>>([]) return ( <div> <Button onClick={async () => { setFalag(true) let arr:number[] = [] console.time() for(let i = 0; i < 5000; i++){ arr.push(i) } await setList(arr) console.timeEnd() }} >渲染</Button> { flag && list.map((item) => <Item id={item} key={item} />) } </div> ); } export default Index;
这里的Item是我们的子组件,也就是一行一行的数据,为了大家更好的看到事件,我特意做了个按钮来控制列表的长度,这里我们假设有五万条数据,通过console.time()和 console.timeEnd()计算一下加载这五万条数据需要多长时间?
可以看到加载的时间大概为2.7s,这样的速度明显达不到要求,而且在真实情况下很容易出现白屏,卡顿的情况,这明显不是我们想要的情况~
分片渲染
分片渲染:简单的说就是一个执行完再执行下一个,其思想是建立一个队列,通过定时器来进行渲染,比如说一共有3次,先把这三个放入到数组中,当第一个执行完成后,并剔除执行完成的,在执行第二个,直到全部执行完毕,渲染队列清空。
利用定时器
我们可以通过设置定时器来进行渲染,通过设置一个等待队列(waitList)和是否渲染(isRender)的条件去做。(在这里我把定时器的时间设置为500,方便演示)
HOC
import { useEffect, useState } from 'react'; import { DotLoading } from 'antd-mobile'; const waitList:any = [] //等待队列 let isRender:boolean = false //控制渲染条件 const waitRender = () => { const res = waitList.shift() if(!res) return setTimeout(() => { res() }, 500) //为演示效果加入一个延长时间 } const HOC = (Component:any) => (props:any) => { const [show, setShow] = useState<boolean>(false) useEffect(() => { waitList.push(() => {setShow(true)}) if(!isRender){ waitRender() isRender = true } }, []) return show ? <Component waitRender={waitRender} {...props}/> : <div style={{margin: 25}}><DotLoading color='primary' />加载中</div> } export default HOC;
代码示例:
import React,{ useEffect, useState } from 'react'; import img from './img.jpeg' import { SlicingHoc } from '@/components'; // 子组件 const Item:React.FC<{id: number, waitRender: () => void}> = ({id, waitRender}) => { useEffect(() => { waitRender() }, []) return ( <div style={{display: 'flex', alignItems: 'center', padding: 5}}> <img src={img} width={80} height={60} alt="" />列表{id} </div> ) } const ItemHoc = SlicingHoc(Item) const Index:React.FC<any> = (props)=> { const [list, setList] = useState<Array<number>>([]) useEffect(() => { let arr:number[] = [] for(let i = 0; i < 5000; i++){ arr.push(i) } setList(arr) }, []) return ( <div> { list.map((item) => <ItemHoc id={item} key={item} />) } </div> ); } export default Index;
效果:
就能实现这样的效果,这个主要有以下两个缺点:
在这个组件中,我们需要通过HOC传递一个waitRender()方法来记录整个队列,也就是说我们要用这个高阶组件,还必须要改变字组件的结构,这点并不是特别好 我们发现这种情况是进行一个一个渲染,上面的没有执行完,下面的是不会渲染的,也就会造成如下效果(定时器设置为0也一样)如:进行改造
分析:
1.针对上述的第一点,我们可以把里面的数组包装成一个整体,通过HOC去循环 2.针对第二点,我们没有必要一个一个进行渲染,可以一次渲染100个,这样渲染的速度就会加快
HOC:
import { useEffect, useState } from 'react'; let waitList:any = [] //等待队列 const HOC = (Component:any) => ({list, ...props}:any) => { const [data, setData] = useState<any>([]) useEffect(() => { if(list.length !== 0){ sliceTime(list, 0) } }, [list]) const sliceTime = (list:any[], times = 0, number:number = 100) => { if(times === (Math.ceil(list.length / number) + 1)) return //判断条件 setTimeout(() => { const newList:any = list.slice(times * number, (times + 1) * number) waitList = [...waitList, ...newList] setData(waitList) sliceTime(list, times + 1) }, 500); } if(list.length === 0) return <></> return <>{ data.map((item:any) => <Component id={item} {...props} key={item} />) }</> } export default HOC;
代码展示:
import React,{ useEffect, useState } from 'react'; import img from './img.jpeg' import { SlicingHoc } from '@/components'; // 子组件 const Item:React.FC<{id: any}> = ({id}) => { return ( <div style={{display: 'flex', alignItems: 'center', padding: 5}}> <img src={img} width={80} height={60} alt="" />列表{id} </div> ) } const ItemHoc = SlicingHoc(Item) const Index:React.FC<any> = (props)=> { const [list, setList] = useState<Array<number>>([]) useEffect(() => { let arr:number[] = [] for(let i = 0; i < 50000; i++){ arr.push(i) } setList(arr) }, []) return ( <div> <ItemHoc list={list} /> </div> ); } export default Index;
效果:
此时,你就会发现渲染的速度会快很多,也没有太大的卡顿,效果而言还算不错~
当然,你可以根据实际情况去设置一次渲染的次数,把HOC定制为公共化,具体的操作可以参考一下这篇文章:作为一名React,我是这样理解HOC的-定制为公用HOC
虚拟列表
我们发现分片渲染有一个根本问题,就是依次渲染,将庞大的数据切分开,然后按顺序依次渲染
但大多数人进入到列表页面,根本不会将整个列表全部看完,从某种角度上来说,像这种全部渲染的情况比较鸡肋,所以在大多数情况下,会采取虚拟列表的形式
虚拟列表:实际上是一种实现方案,只对可视区域进行渲染,对非可视区域中的区域不渲染或只渲染一部分(渲染的部分叫缓冲区,不渲染的部分叫虚拟区),从而达到极高的性能
简单分析
我们先看一下下方的图(由于我的图画的实在难看,所以在网上找了一张比较符合的,还望勿喷~)
从图中可以看出,我们可以将列表分为三个区域:可视区、缓冲区、虚拟区
而我们主要针对可视区和缓冲取进行渲染,我们一步一步的实现,有不对的地方,希望在评论区指出~
页面布局
在这个组件中,首先要做的事情就是布局,我们需要有两块区域:
占位区域:聪明的小伙伴发现,在上述的分片渲染中,滚动条也在变化,这是因为列表渲染的数据在增加,把内容组件撑开,造成高度上的变化,所以在虚拟列表中,专门提供一个div,用来占位,这样在一进来的时候滚动条就不会产生变化 渲染区域:这块部分为真正用户看到的列表区域,实际上有可视区和缓冲区共同组成,缓冲区的作用是防止快速下滑或者上滑的过程中出现空白区域其次我们需要一个整体的div,通过监听占位区域的滚动条,判断当前截取数组的区域,所以大体的结构是这样
<div ref={allRef}> <div ref={scrollRef} > {/* 占位,列表的总高度,用于生成滚动条 */} <div></div> {/* 内容区域 */} <div> {/* 渲染区域 */} { state.data.map((item:any) => <div key={item}> {/* 子组件 */} <Component id={item} {...props}/> </div>) } </div> </div> </div>
参数计算
我们可以将需要的元素进行总结,在这里,我会使用useReactive来存取参数,是一种具备响应式的useState,关于这个的实现,可参考:搞懂这12个Hooks,保证让你玩转React-useReactive
相关容器的高度:
列表总共的个数(总列表数):设置为 list
容器的高度: 当前组件所占的位置(可通过传值控制)scrollAllHeight = allRef.current.offsetHeight
子列表高度:子组件的高度(这个如何获取,后续讲到,案例中为65)ItemHeight = 65
占位区域高度,也就是整个列表的高度,用于生成滚动条:占位区域高度 = 子列表高度 * 列表总数个数
listHeight = ItemHeight * list.length
渲染区域的计算点:其实我们渲染的数据只是可视区和缓冲区,我们可以利用slice对list进行截取,所以在我们还需要知道:
索引的起始位置:start 索引的结束位置:end 缓冲个数:bufferCount 需要渲染的节点数量(可视区能渲染几个节点)渲染节点的数量 = 容器的高度 / 子列表高度 (需要向上取整) + 缓冲个数
const renderCount = Math.ceil(scrollAllHeight / ItemHeight)+ state.bufferCount
滚动区域
在这里我使用useEventListener去监听滚动事件,是一个可以监听任何函数的自定义hooks,具体实现可参考:搞懂这12个Hooks,保证让你玩转React-useEventListener
我们要拿到滚动条距离顶部的高度,然后计算对应的索引起始和结束位置,再截取对应的数据给到data就OK了,并且计算对应的偏移量,也就是:
useEventListener('scroll', () => { // 顶部高度 const { scrollTop } = scrollRef.current state.start = Math.floor(scrollTop / state.itemHeight) state.end = Math.floor(scrollTop / state.itemHeight + state.renderCount + 1) state.currentOffset = scrollTop - (scrollTop % state.itemHeight) state.data = list.slice(state.start, state.end) }, scrollRef)
优化
经过上面的讲解,我们可以发现,高阶组件渲染的数据实际上只有state.data,数据的变化是由滚动事件所引起的,造成start和end的改变,所以在这里,我们可以使用useCreation来进行优化,useCreation相当于是升级版的useMemo,具体实现,可以参考搞懂这12个Hooks,保证让你玩转React-useCreation
useCreation(() => { state.data = list.slice(state.start, state.end) }, [state.start])
这样,一个简易版的虚拟列表就ok了
代码展示
HOC:
import { useEffect, useRef } from 'react'; import useReactive from '../useReactive' import useEventListener from '../useEventListener' import useCreation from '../useCreation' const HOC = (Component:any) => ({list, ...props}:any) => { const state = useReactive({ data: [], //渲染的数据 scrollAllHeight: '100vh', // 容器的初始高度 listHeight: 0, //列表高度 itemHeight: 0, // 子组件的高度 renderCount: 0, // 需要渲染的数量 bufferCount: 6, // 缓冲的个数 start: 0, // 起始索引 end: 0, // 终止索引 currentOffset: 0, // 偏移量 }) const allRef = useRef<any>(null) // 容器的ref const scrollRef = useRef<any>(null) // 检测滚动 useEffect(() => { // 子列表高度 const ItemHeight = 65 // 容器的高度 const scrollAllHeight = allRef.current.offsetHeight // 列表高度 const listHeight = ItemHeight * list.length; //渲染节点的数量 const renderCount = Math.ceil(scrollAllHeight / ItemHeight) + state.bufferCount state.renderCount = renderCount state.end = renderCount + 1 state.listHeight = listHeight state.itemHeight = ItemHeight state.data = list.slice(state.start, state.end) }, [allRef]) useCreation(() => { state.data = list.slice(state.start, state.end) }, [state.start]) useEventListener('scroll', () => { // 顶部高度 const { scrollTop } = scrollRef.current state.start = Math.floor(scrollTop / state.itemHeight) state.end = Math.floor(scrollTop / state.itemHeight + state.renderCount + 1) state.currentOffset = scrollTop - (scrollTop % state.itemHeight) // state.data = list.slice(state.start, state.end) }, scrollRef) return <div ref={allRef}> <div style={{height: state.scrollAllHeight, overflow: 'scroll', position: 'relative'}} ref={scrollRef} > {/* 占位,列表的总高度,用于生成滚动条 */} <div style={{ height: state.listHeight, position: 'absolute', left: 0, top: 0, right: 0 }}></div> {/* 内容区域 */} <div style={{ transform: `translate3d(0, ${state.currentOffset}px, 0)`, position: 'relative', left: 0, top: 0, right: 0}}> {/* 渲染区域 */} { state.data.map((item:any) => <div key={item}> {/* 子组件 */} <Component id={item} {...props} /> </div>) } </div> </div> </div> } export default HOC;
页面代码
import React,{ useEffect, useState } from 'react'; import img from './img.jpeg' import { HOC } from '@/components'; // 子组件 const Item:React.FC<{id: any}> = ({id}) => { return ( <div style={{display: 'flex', alignItems: 'center', padding: 5}}> <img src={img} width={80} height={60} alt="" />列表{id} </div> ) } const ItemHoc = HOC(Item) const Index:React.FC<any> = (props)=> { const [list, setList] = useState<Array<number>>([]) useEffect(() => { let arr:number[] = [] for(let i = 0; i < 500; i++){ arr.push(i) } setList(arr) }, []) if(list.length === 0) return <></> return ( <div> <ItemHoc list={list} /> </div> ); } export default Index;
效果
虚拟列表-可优化的方向
下拉请求数据
海量的数据可能用户并不会看完,需要下拉到底部进行刷新,所以我们可以判断一个临界值:滚动条距离底部的距离为0时出发
临界值:距离底部的高度 = 滚动条的高度 - 默认的高度 - 距离顶部的高度
const button = scrollHeight - clientHeight - scrollTop
然后我们传递给外界一个方法,做请求事件即可:onRequest(请求完拼接到list即可)
useEventListener('scroll', () => { // 顶部高度 const { clientHeight, scrollHeight } = scrollRef.current // 滚动条距离的高度 const button = scrollHeight - clientHeight - scrollTop if(button === 0 && onRequest){ onRequest() } }, scrollRef)
效果:
子列表高度问题
在上面的代码中,其实有一个很重要的问题,就是子列表的高度,我在上述的过程中,实际上是写死的
但在实际的开发过程中,子列表的高度有两种情况:定高和不定高
定高很简单,我们只需要手动计算下列表的高度,将值传入就行,但不定高就很麻烦了,因为你无法计算出每个高度的情况,导致列表的整体高度、偏移量都无法正常的计算
在这里我用mock来模拟些数据看看:
思考对于子列表的动态高度我们该如何处理?
1.第一种,将ItemHeight作为参数传递过来,我们可以根据传递数组来控制,但这种情况需要我们提前将列表的高度算出来,算每个子列表的高度很麻烦,其次这个高度还要根据屏幕的大小去变化,这个方法明显不适合
2.第二种,预算高度,我们可以假定子列表的高度也就是虚假高度(initItemHeight),当我们渲染的时候,在更新对应高度,这样就可以解决子列表高度的问题
预算高度该如何考虑针对第二种方案,我们需要去维护一个公共的高度列表(positions),这个数组将会记录真实的DOM高度
那么positions需要记录那些信息:
const state = useReactive<any>({ ..., positions: [ //需要记录每一项的高度 // index // 当前pos对应的元素的下标 // top; // 顶部位置 // bottom // 底部位置 // height // 元素高度 // dHeight // 用于判断是否需要改变 ], initItemHeight: 50, // 预计高度 })
需要记录元素的高度,其次可以存入距离顶部和底部的高度,方便后面计算偏移量和列表的整体高度,在设定一个参数(dHeight)判断新的高度与旧的高度是否一样,不一样的话就进行更新
其中最重要的就是index,它用来记录子列表真实高度的下标,这个点极为重要,原因是:在之前的讲解中,我们发现start 和 end的差值实际上是不变的,也就是说,最终渲染的数据,实际上是一个固定值,但里面的子列表高度却是变值,所以我们需要有一个变量来区分数据所对应的高度,所以这个index就变的尤为重要
所以在这里我们设置一个ref用来监听子节点node,来获取真实高度,这里我设置id来判断对应的索引
// list:数据改变 let arr:any[] = [] for(let i = 0; i < 100; i++){ arr.push({ id: i, //设置唯一值 content: Mock.mock('@csentence(40, 100)') // 内容 }) } setList(arr) // 渲染数据 {/* 内容区域 */} <div ref={ref} style={{ transform: `translate3d(0, ${state.currentOffset}px, 0)`, position: 'relative', left: 0, top: 0, right: 0}}> {/* 渲染区域 */} { state.data.map((item:any) => <div id={String(item.id)} key={item.id}> {/* 子组件 */} <Component id={item.content} {...props} index={item.id} /> </div>) } </div> //初始的positions useEffect(() => { // 初始高度 initPositions() }, []) const initPositions = () => { const data = [] for (let i = 0; i < list.length; i++) { data.push({ index: i, height: state.initItemHeight, top: i * state.initItemHeight, bottom: (i + 1) * state.initItemHeight, dHeight: 0 }) } state.positions = [...data] } 初始计算
我们要改变的是子列表的高度和列表的高度
useEffect(() => { // 子列表高度:为默认的预计高度 const ItemHeight = state.initItemHeight // // 容器的高度 const scrollAllHeight = allRef.current.offsetHeight // 列表高度:positions最后一项的bottom const listHeight = state.positions[state.positions.length - 1].bottom; //渲染节点的数量 const renderCount = Math.ceil(scrollAllHeight / ItemHeight) state.renderCount = renderCount state.end = renderCount + 1 state.listHeight = listHeight state.itemHeight = ItemHeight state.data = list.slice(state.start, state.end) }, [allRef, list.length])
这里要注意一点的是:预计高度尽量要小点,可以多加载,但不能少,防止渲染不全
更新具体的高度当我们第一遍把列表的数据渲染成功后,就更新positions的高度,将真实的高度替换一开始的虚拟高度,并将整体的高度进行更新
useEffect(() => { setPostition() }, [ref.current]) const setPostition = () => { const nodes = ref.current.childNodes if(nodes.length === 0) return nodes.forEach((node: HTMLDivElement) => { if (!node) return; const rect = node.getBoundingClientRect(); // 获取对应的元素信息 const index = +node.id; // 可以通过id,来取到对应的索引 const oldHeight = state.positions[index].height // 旧的高度 const dHeight = oldHeight - rect.height // 差值 if(dHeight){ state.positions[index].height = rect.height //真实高度 state.positions[index].bottom = state.positions[index].bottom - dHeight state.positions[index].dHeight = dHeight //将差值保留 } }); // 重新计算整体的高度 const startId = +nodes[0].id const positionLength = state.positions.length; let startHeight = state.positions[startId].dHeight; state.positions[startId].dHeight = 0; for (let i = startId + 1; i < positionLength; ++i) { const item = state.positions[i]; state.positions[i].top = state.positions[i - 1].bottom; state.positions[i].bottom = state.positions[i].bottom - startHeight; if (item.dHeight !== 0) { startHeight += item.dHeight; item.dHeight = 0; } } // 重新计算子列表的高度 state.itemHeight = state.positions[positionLength - 1].bottom; }
进行下对比:
这样就可以将真实的高度替换虚拟的高度
除了首次的渲染之外,还有就是在start或end改变时重新计算,也就是
useCreation(() => { state.data = list.slice(state.start, state.end) if(ref.current){ setPostition() } }, [state.end]) 计算偏移量
在滚动的方法中,我们可以通过二分查找去降低检索次数,同时我们每次的偏移量为state.positions[state.start - 1].bottom
useEventListener('scroll', () => { // 顶部高度 const { scrollTop, clientHeight, scrollHeight } = scrollRef.current state.start = binarySearch(state.positions, scrollTop); state.end = state.start + state.renderCount + 1 // 计算偏移量 state.currentOffset = state.start > 0 ? state.positions[state.start - 1].bottom : 0 // 滚动条距离的高度 const button = scrollHeight - clientHeight - scrollTop if(button === 0 && onRequest){ onRequest() } }, scrollRef) // 二分查找 const binarySearch = (list:any[], value: any) =>{ let start:number = 0; let end:number = list.length - 1; let tempIndex = null; while(start <= end){ let midIndex = parseInt(String( (start + end)/2)); let midValue = list[midIndex].bottom; if(midValue === value){ return midIndex + 1; }else if(midValue < value){ start = midIndex + 1; }else if(midValue > value){ if(tempIndex === null || tempIndex > midIndex){ tempIndex = midIndex; } end = end - 1; } } return tempIndex; } 代码展示
HOC:
import { useEffect, useRef } from 'react'; import useReactive from '../useReactive' import useEventListener from '../useEventListener' import useCreation from '../useCreation' const HOC = (Component:any) => ({list, onRequest, ...props}:any) => { const state = useReactive<any>({ data: [], //渲染的数据 scrollAllHeight: '100vh', // 容器的初始高度 listHeight: 0, //列表高度 itemHeight: 0, // 子组件的高度 renderCount: 0, // 需要渲染的数量 bufferCount: 6, // 缓冲的个数 start: 0, // 起始索引 end: 0, // 终止索引 currentOffset: 0, // 偏移量 positions: [ //需要记录每一项的高度 // index // 当前pos对应的元素的下标 // top; // 顶部位置 // bottom // 底部位置 // height // 元素高度 // dHeight // 用于判断是否需要改变 ], initItemHeight: 50, // 预计高度 }) const allRef = useRef<any>(null) // 容器的ref const scrollRef = useRef<any>(null) // 检测滚动 const ref = useRef<any>(null) // 检测滚动 useEffect(() => { // 初始高度 initPositions() }, []) const initPositions = () => { const data = [] for (let i = 0; i < list.length; i++) { data.push({ index: i, height: state.initItemHeight, top: i * state.initItemHeight, bottom: (i + 1) * state.initItemHeight, dHeight: 0 }) } state.positions = [...data] } useEffect(() => { // 子列表高度:为默认的预计高度 const ItemHeight = state.initItemHeight // // 容器的高度 const scrollAllHeight = allRef.current.offsetHeight // 列表高度:positions最后一项的bottom const listHeight = state.positions[state.positions.length - 1].bottom; //渲染节点的数量 const renderCount = Math.ceil(scrollAllHeight / ItemHeight) state.renderCount = renderCount state.end = renderCount + 1 state.listHeight = listHeight state.itemHeight = ItemHeight state.data = list.slice(state.start, state.end) }, [allRef, list.length]) useEffect(() => { setPostition() }, [ref.current]) const setPostition = () => { const nodes = ref.current.childNodes if(nodes.length === 0) return nodes.forEach((node: HTMLDivElement) => { if (!node) return; const rect = node.getBoundingClientRect(); // 获取对应的元素信息 const index = +node.id; // 可以通过id,来取到对应的索引 const oldHeight = state.positions[index].height // 旧的高度 const dHeight = oldHeight - rect.height // 差值 if(dHeight){ state.positions[index].height = rect.height //真实高度 state.positions[index].bottom = state.positions[index].bottom - dHeight state.positions[index].dHeight = dHeight //将差值保留 } }); // 重新计算整体的高度 const startId = +nodes[0].id const positionLength = state.positions.length; let startHeight = state.positions[startId].dHeight; state.positions[startId].dHeight = 0; for (let i = startId + 1; i < positionLength; ++i) { const item = state.positions[i]; state.positions[i].top = state.positions[i - 1].bottom; state.positions[i].bottom = state.positions[i].bottom - startHeight; if (item.dHeight !== 0) { startHeight += item.dHeight; item.dHeight = 0; } } // 重新计算子列表的高度 state.itemHeight = state.positions[positionLength - 1].bottom; } useCreation(() => { state.data = list.slice(state.start, state.end) if(ref.current){ setPostition() } }, [state.end]) useEventListener('scroll', () => { // 顶部高度 const { scrollTop, clientHeight, scrollHeight } = scrollRef.current state.start = binarySearch(state.positions, scrollTop); state.end = state.start + state.renderCount + 1 // 计算偏移量 state.currentOffset = state.start > 0 ? state.positions[state.start - 1].bottom : 0 // 滚动条距离的高度 const button = scrollHeight - clientHeight - scrollTop if(button === 0 && onRequest){ onRequest() } }, scrollRef) // 二分查找 const binarySearch = (list:any[], value: any) =>{ let start:number = 0; let end:number = list.length - 1; let tempIndex = null; while(start <= end){ let midIndex = parseInt(String( (start + end)/2)); let midValue = list[midIndex].bottom; if(midValue === value){ return midIndex + 1; }else if(midValue < value){ start = midIndex + 1; }else if(midValue > value){ if(tempIndex === null || tempIndex > midIndex){ tempIndex = midIndex; } end = end - 1; } } return tempIndex; } return <div ref={allRef}> <div style={{height: state.scrollAllHeight, overflow: 'scroll', position: 'relative'}} ref={scrollRef} > {/* 占位,列表的总高度,用于生成滚动条 */} <div style={{ height: state.listHeight, position: 'absolute', left: 0, top: 0, right: 0 }}></div> {/* 内容区域 */} <div ref={ref} style={{ transform: `translate3d(0, ${state.currentOffset}px, 0)`, position: 'relative', left: 0, top: 0, right: 0}}> {/* 渲染区域 */} { state.data.map((item:any) => <div id={String(item.id)} key={item.id}> {/* 子组件 */} <Component id={item.content} {...props} index={item.id} /> </div>) } </div> </div> </div> } export default HOC;
页面代码:
import React,{ useEffect, useState } from 'react'; import { HOC } from '@/components'; import Mock from 'mockjs'; // 子组件 const Item:React.FC<{id: any, index?:number}> = ({id, index}) => { return ( <div style={{display: 'flex', alignItems: 'center', padding: 5, lineHeight: '24px', border: '1px solid #ccc'}}> 列表{index}: {id} </div> ) } const ItemHoc = HOC(Item) const Index:React.FC<any> = (props)=> { const [list, setList] = useState<any>([]) useEffect(() => { let arr:any[] = [] for(let i = 0; i < 100; i++){ arr.push({ id: i, content: Mock.mock('@csentence(40, 100)') }) } setList(arr) }, []) if(list.length === 0) return <></> return ( <div> <ItemHoc list={list} /> </div> ); } export default Index; 效果
存在的问题
列表中可能存在由图片撑起高度的情况,图片会发送网络请求,可能会造成计算不准确的问题,但这种情况比较少见,基本上可以忽略
End
本文是将两种常见的分片渲染和虚拟列表的功能封装成高阶组件的形式,与传统的懒加载还是有一定的区别,本质上有所不同
本文中考虑的可能并不是非常的全面,有任何好的建议,欢迎再评论区指出~
如果你耐心的看完本文,相信你对长列表的处理方式有了一些理解,建议还是亲自动手试一试,提升下自己,如果觉得本文还算不错,请【点赞】+ 【收藏】,另外可多多关注笔者的专栏:深入React,彻底搞懂React,后续会有比较硬核的React文章,还请各位小伙伴多多支持~
其他React好文:
搞懂这12个Hooks,保证让你玩转React 作为一名React,我是这样理解HOC的 「React深入」一文吃透虚拟DOM和diff算法 「React 深入」一文吃透React v18全部Api(1.3w+)玩转 React Hooks 小册
小册链接:《玩转 React Hooks》
知其然,知其所以然。React Hooks 带来的全新机制让人耳目一新,因为它拓展了 React 的开发思路,为 React 开发者提供了一种更方便、更简洁的选择。
在引入 Hooks 的概念后,函数组件既保留了原本的简洁,也具备了状态管理、生命周期管理等能力,在原来 Class 组件所具备的能力基础上,还解决了 Class 组件存在的一些代码冗余、逻辑难以复用等问题。因此,在如今的 React 中,Hooks 已经逐渐取代了 Class 的地位,成了主导。
而且,Hooks 相对于 Class 而言,更容易上手,其简洁性、逻辑复用性等特性深受开发者喜爱,可谓是前端界的"流量明星",不止 React,Vue 3.0 、Preact、Solid.js 等框架也都选择加入 Hooks 的大家庭,前端的日常工作也在趋向于 Hooks 开发。
因此,掌握好 React Hooks 是非常有必要的一件事。本小册会通过基础篇、原码篇、实践篇 三大方向 探讨 Hooks,从原码的角度探寻 React 的奥秘。
除此之外,小册会以 React Hooks 为核心,同时穿插其他知识,如 TS、Jest、Fiber 等核心知识,并包含 React v18 的并发、数据撕裂等概念,最后结合 Hooks 写一个简易版 react-redux 和 Form 表单,通过其设计思想,助你在面试中脱颖而出。
小册整体设计如下思维导图所示:
你会学到什么?
全面知悉 React 提供的 15 Hooks API 的使用和场景; 手写 30+ 自定义 Hooks 的实现,全面掌握设计思想; 了解 Hooks 源码,从根源上彻底解决现有的难点; 掌握函数式编程思想,用于工作,享受便利。最后
感谢各位小伙伴的支持,如果在阅读过程中有什么问题欢迎大家加我微信,交个朋友,微信:domesyPro, 也可以关注笔者的公众号:杜杜的全栈之旅,一起来玩转 React Hooks 吧~
相关知识
在 Unity 中设置渲染管线和光照
基于微信小程序的鲜花销售(毕业设计,包括源码,数据库,教程).zip
Vue.js 增删查改库存管理系统教程:新手友好的完整代码与详细步骤」 「从零开始的库存管理系统:Vue.js 实现搜索、编辑、删除与数据更新」 「超详细的 Vue.js CRUD 教程:带你一步步构
理解Python中列表/字典/元组/集合
景观设计渲染软件有哪些?哪个好用?
在实时渲染过程中,如何利用GPU技术实现高质量的全局光照效果?请参考《GPUPro 1:高级渲染技术》的相关章节提供详细解答。
如何在实时渲染中实现高质量的全局光照效果?请结合《GPUPro 1:高级渲染技术》中的相关章节进行解答。
生命的舒展与心灵的表情 赏读杜平让的花卉世界
渲染恐怖气氛的句子
擅长画花卉画的画家大全 静物油画家列表
网址: 花三个小时,完全掌握分片渲染和虚拟列表~大家好,我是小杜杜,有关高性能,大数据量的列表渲染的示例已经非常常见,可以说是前 https://www.huajiangbk.com/newsview855656.html
上一篇: 三年级滚来滚去的小土豆作文300 |
下一篇: 小土豆历险记作文(精选16篇) |
推荐分享

- 1君子兰什么品种最名贵 十大名 4012
- 2世界上最名贵的10种兰花图片 3364
- 3花圈挽联怎么写? 3286
- 4迷信说家里不能放假花 家里摆 1878
- 5香山红叶什么时候红 1493
- 6花的意思,花的解释,花的拼音 1210
- 7教师节送什么花最合适 1167
- 8勿忘我花图片 1103
- 9橄榄枝的象征意义 1093
- 10洛阳的市花 1039