unity循环滚动列表_长列表60FPS?稳如老狗!

一个真正的实现了DOM节点高效回收和复用的高性能虚拟长列表组件

海量数据场景下的性能瓶颈

几乎可以肯定的是,如果在前端场景下直接渲染海量数据,其体验基本上是很糟糕的,其主要原因如下

  1. 海量数据导致大量的DOM节点,从而导致DOM在初始化或者reflow、repaint的时候发生大量的计算,导致卡帧,掉帧
  2. 大量的DOM节点导致占用了大量的内存

现有的解决方案及其原理

  1. react-virtualized
  2. react-window

这两个组件本质上一种方案,它最核心的点在于只渲染了当前视口及其上下一定范围内的节点,从而做到了DOM节点数量的范围是固定的,而不是和数据量成正比

其基本的实现原理

其所需要的必要条件

  1. 固定尺寸的滚动容器
  2. 固定的数据长度
  3. 每个数据对应的组件高度可以被计算

这样的话就可以计算出以下的数据

  1. 整个滚动内容的高度可以被计算出来 - totalHeight
  2. 滚动至某个位置的时候根据scrollTop计算出当前容器视口下所能展现的items

具体实现

  1. 最外层容器固定宽高
  2. 内层的div高度为totalHeight,撑起整个滚动内容
  3. 根据scrollTop计算出来需要显示的items以绝对定位的方式定位在内层的div上
  4. item的key是和对应数据一一对应的,这样不需要的item将被删除,继续渲染的item节点被复用

71a45cc3dea9f842fc48d27039ee9eda.png

这样,就实现了真正渲染的dom节点和容器的高度成正比而不是和数据量成正比。
从而解决了上述所说的性能瓶颈。

不足之处

这种方式虽然能基本解决上述的性能瓶颈,但是实际使用下来还是感觉性能一般,在实际体验中还是能感觉到有_短暂的白屏 _这种现象。

在高速滚动的过程中,为了维持当前DOM节点的规模,会频繁的发生新的DOM节点被插入旧的DOM被删除,不仅导致大量的cpu开销,和频繁的GC以及内存抖动。从而,导致实际的体验不如人意。

重新思考使用场景

在实际的使用过程中,我们列表中的item类型是有限的,如果我们在滚动过程中回收旧的节点,并且在插入新节点的时候复用相同类型的旧节点,那么将会节约大量的开销,进一步提升性能。

那么,最大的问题就是:在React规则下,我们要怎么实现节点的回收和复用呢?

答案就是巧妙的使用 key,具体稍后会将。

重新设计列表模型以按类型区分节点

  • 规范列表数据
type CellDatas = {
  height: number; // 数据对应节点的高度
  data: any; // 所需要传入组件的数据

  // 节点类型,也是真正需要 render 的组件
  Component: React.ComponentType<CellWrapperProps>; 
}[];
  • 怎么回收旧的节点呢?

很简单。本来旧的节点消失在视口一定距离之后应该销毁,此时我们不对这个节点进行销毁,
让其留在原来的位置,当插入新的节点中有相同类型的组件(节点)时,直接将其(旧节点)拿来复用。

  • 怎么复用旧的节点呢。

------ 本文最核心的逻辑 ------实际上,在我们的节点采取绝对定位之后,节点所显示出来的顺序其实和其在DOM中节点的顺序(又或者说其在React列表中的顺序)是完全无关的,节点所显示出来的顺序完全取决于样式也就是style.top属性。所以,新的节点复用旧的节点只要保持key与之前的节点一致,并且传入新的 style 和 data 即可。这样,旧的节点就在显示效果上 变成 了新的节点,而且没有发现dom的销毁与重建,仅仅付出了修改style和部分dom的开销。

新思路下实现的组件逆天的性能表现

https://weird94.github.io/list/normal/

2000 条数据 2种组件类型在chrome 6 * slowdown情况下性能
60FPS稳如老狗,内存也稳定在3-7MB内,超快速滚动也未出现短暂白屏现象。

a569bbe19d408154b90850eeddcb219c.png
https://www.zhihu.com/video/1225817241557942272

而且,从 devtools 的 elements 面板可以看出, 列表item项的DOM节点只有style发生了变化,
节点本身没有发生销毁重建,说明旧的节点得到了很好的复用。

react-recyclelist 组件的具体实现

数据结构

  • 根据数据 CellDatas 计算出来的节点布局信息
type Layouts = {
  height: number;
  top: number;
  type: React.ComponentType<CellProps<any>>;
}[];
  • 存放当前正在render的节点和已经回收了的节点池
type RenderInfo = {
    i: number; // 数据指针,用于查找需要渲染的组件和数据
  dom: number; // 节点在DOM中的顺序,用来保证
};

// 列表滚动位置 scrollTop 计算出应该渲染节点的起始位置start-end
// start-end区域需要被渲染的节点信息
let current: RenderInfo[];

/*
* 节点池
* 以Map的形式组织
* key 为 CellData.type 也就是React.ComponetType
* value 为有序 RenderInfo 数组,存放着所回收dom的数据指针(i) 和在DOM中的位置(dom)
*/
class NAMap {};

// current 上方区域回收的节点池
let topRecycleMap: NAMap;

// current 下方区域的节点池
let bottomRecycleMap: NAMap;
  • 真正需要渲染的DOM节点
let renderCurrent: number[];
// 真正需要被渲染的节点
// 等于 topRecycleMap + current + bottomRecycleMap
// 然后按 dom 从小到大排序

renderCurrent = [
  ...this.topRemoveMap.getList(),
  ...current,
  ...this.bottomRemoveMap.getList()
]
  .sort((a, b) => a.dom - b.dom)
  .map(i => i.i);

工作逻辑

简单的来说

  1. 划分两个回收池,分别存储当前渲染区域上下区域所回收的节点
  2. 当向下滚动的时候 查找可复用节点顺序为 先在 bottomRecycleMap 执行队头查找(getFirst),如果没找到再topRecycleMap查找执行队头查找(getFirst)。(先复用下方最近的节点,再复用上方最远节点
  3. 向上滚动时,先 topRecycleMap.getLast,如果没找到再 bottomRecycleMap.getLast。
  4. 如果没找到复用节点,就插入一个新节点
  5. 最后,按key顺序排序,防止因为key顺序变动导致的dom节点出现了删除又插入的不必要开销。

示例

15cf06eaf513f74845523fe026009af6.png

从 _状态1 _滚动到 _状态2 _时

  1. A1 B2已经滚动到上方可回收的位置时,将它们加入回收池,此时回收池为
topRecycleMap 
        A: 1 -> null
    B: 2 -> null

topRecycleMap(空)
  1. 同时,current 中最后一个item已经出现 top+height < scrollTop + height * 渲染因子
  2. 应该向 current 中循环增加新的节点以满足充满整个渲染区域 start - end
  3. 第五个位置类型为B,发现 topRecycleMap 中存在一个可复用的节点,拿来复用,当前节点key则为 1
  4. 第六个第七个节点时,已经没有可以复用的节点,则直接插入新的节点
  5. 此时,当前内容范围已经满足需求,停止增加节点
  6. 最后,将回收池(topRecycleMap、bottomRecycleMap)已经渲染区域(current)的节点按key进行排序渲染。

相关资源

组件源码: react-typed-recyclelist

PS: 厚脸皮求个star~

LiveDemo:

https://weird94.github.io/list/normal/​weird94.github.io https://weird94.github.io/list/statefull/​weird94.github.io https://weird94.github.io/list/resort/​weird94.github.io
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity中实现无限循环滚动列表的方法有多种,其中较为常见的是使用Object Pool技术和ScrollRect组件。 1. Object Pool技术 Object Pool是一种对象池技术,可以提高游戏中对象的创建和销毁效率,减少内存的开销。在无限循环滚动列表中,可以使用Object Pool技术来管理列表中的所有项,当列表项超出可见范围时,将其回收到对象池中,再从对象池中获取新的列表项填充空缺。 具体实现步骤如下: (1)创建一个空的GameObject作为对象池的父物体。 (2)创建一个列表项的Prefab,并将其添加到对象池中。 (3)在ScrollRect组件中添加滑动事件监听,当滑动结束时,通过计算当前可见区域内的列表项的索引范围,将超出范围的列表项回收到对象池中,并从对象池中获取新的列表项填充空缺。 (4)在列表项的脚本中,添加一个回收自身的方法,将自身回收到对象池中。 (5)在对象池中,添加一个获取可用列表项的方法,在需要新的列表项时,从对象池中获取可用的列表项,如果没有可用的列表项,就创建新的列表项并添加到对象池中。 2. ScrollRect组件 Unity中提供了一个ScrollRect组件,可以方便地实现滚动列表功能。在无限循环滚动列表中,可以通过设置Content的大小和位置,使其在滚动时无限循环。 具体实现步骤如下: (1)创建一个空的GameObject作为滚动列表的父物体。 (2)在父物体上添加一个ScrollRect组件,并设置其滑动方向和滚动条样式。 (3)在ScrollRect中创建一个Content物体,并设置其大小和位置,在滚动时,Content会无限循环滚动,并且在滚动到边界时,会自动调整Content的位置。 (4)在Content中添加列表项,并设置其位置和大小,在滚动时,列表项会随着Content一起滚动,当列表项超出可见范围时,就需要将其回收或者销毁,或者使用Object Pool技术管理列表项。 (5)在列表项的脚本中,添加一个设置自身数据的方法,当滚动列表需要更新数据时,调用列表项的设置数据方法即可。 以上就是Unity中实现无限循环滚动列表的两种常见方法,具体实现方式可以根据项目的需求和实际情况进行选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值