Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

  • 工程能力

  • 应用基础

    • 兼容性

    • 前端安全

    • 国际化

    • 性能优化

      • 如何实现 script 并行异步加载顺序执行
      • yahoo前端优化35军规
      • 前端首屏优化 | 借助客户端能力提升 H5 首屏的 8 个手段
      • 前端优化性能指标
      • 4 种常用的前端性能分析工具
      • 前端性能预算经验
      • 前端图片分层加载详解
      • 基于CDN的前端优化可行性分析
      • 浅谈图片分层加载与懒加载
      • 浅谈 preload 预加载
      • 用 CAP 理论指导 Hybrid App 离线策略优化
      • 长列表

        • rc-virtual-list 源码解析
          • 前言
          • 核心思想
            • 渲染
            • 计算
            • ① 确定起止项和定位项
            • ② 渲染列表项
            • ③ 调整 offset
            • 健壮
            • 总结
        • 为什么不用rAF进行滚动节流
        • 「前端长列表」开源库解析及最佳实践
        • 如何解决长列表的滚动白屏问题
      • 面试官问:如何实现 H5 秒开?
    • 换肤

    • 无障碍

  • 专业领域

  • 业务场景

  • 大前端
  • 应用基础
  • 性能优化
  • 长列表
gahing
2021-09-21
目录

rc-virtual-list 源码解析草稿

# 前言

之前写过一个长列表原理的文章 -- 「前端长列表」开源库解析及最佳实践 (opens new window)

里面提到说 rc-virtual-list (opens new window) 是性能最好问题最少的方案。

并做了一番解析。

但当时只停留在「怎么做」的层面,没有从「为什么这么做」去出发,导致一些小伙伴问我为啥这么处理的时候,我回答不上来

今天就来重新解读下这个项目

# 核心思想

任意高度的列表项都占据相同的滚动条范围

那么

支持自适应高度,支持动画效果,支持滚动位置复原

# 渲染

  <!-- 用户可见的容器高度可能只有 300px -->
  <div
    class="container"
    style="width: 200px; height: 300px;"
    @scroll.passive="handleScroll"
  >
    <!-- 总的列表 div ,用于撑起列表的高度 -->
    <div
      class="total-list"
      :style="{
        height: `${itemHeight * data.length}px`,
      }"
    >
      <div
        class="visible-list"
        :style="{
        transform: `translateY(${topHeight}px)`,
      }"
      >
        <div
          v-for="item in visibleList"
          :key="item.id"
          class="visible-list-item"
          :style="{
          height: `${itemHeight}px`,
        }"
        >{{ item.value }}</div>
      </div>
    </div>

    <!-- 此处只需渲染可见列表即可,无需渲染全部数据 -->

  </div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

和上面的方案不一样,这里是创建了一个 total-list 的容器,直接对这个容器进行 translateY 偏移

# 计算

总高度始终固定,等于 列表项个数(itemCount) * 列表项最小高度(itemHeight)

处理逻辑如下:

  1. 滚动,确定定位项和起止项
  2. 渲染起止列表项(虚拟 dom 到真实 dom)
  3. 列表项渲染完毕,计算并调整起始项偏移位置(回流获取)
  4. 进行重渲染

注意:本处渲染的含义和浏览器实际渲染(UI Render)不同,整个过程的操作和渲染均处在一个宏任务,还未到 UI Render 阶段

核心思想是任意高度的列表项都占据相同的滚动条范围

先上图(建议分屏和下文对照阅读)

在获取到数据(data)后,未滚动前,有哪些值我们是确定的呢?

  1. 列表项个数(itemCount): data.length
  2. 列表项预设高度(itemHeight): 预设值,必须是列表项最小高度
  3. 列表固定总高度(listHeight): itemCount * itemHeight
  4. 容器高度(clientHeight): 预设值,或者通过 element.clientHeight 获得
  5. 滚动条高度(scrollBarHeight): clientHeight * (clientHeight/listHeight)

    滚动条高度/容器高度 = 容器高度/列表固定总高度

  6. 最大可滚动高度(scrollTopMax): listHeight - clientHeight
  7. 滚动条最大偏移量(scrollBarHeightMax): clientHeight - scrollBarHeight
  8. 可见列表项个数(visibleCount): Math.ceil(clientHeight / itemHeight)

    注意必须向上取整;为了保证充满容器,我们以最小高度计算可见列表项个数

先不考虑源码中「定位项」的做法,我们如何确定起始位置(startIndex)呢?

上文说过,我们把每一项都看成是相同高度,所以我们采用固定高度的做法试试:

startIndex = Math.floor(scrollTop/itemHeight)
// 起始项偏移高度
startItemTop =  startIndex * itemHeight
1
2
3

列表渲染完毕时,

这么简单?举个例子验证下

itemCount=50
itemHeight=20
clientHeight=100
visibleCount=5
scrollTopMax=900
=> scrollTop = 0
startIndex=0
endIndex=9
=> scrollTop = 120
startIndex=(120/900)*45

50 项* 20 最小高度, 总高度固定 1000

视口高 100 ,滚动条高度 100 * (100/1000)  =  10,可滚动距离为 90,可滚动的实际高度为 1000 - 100 = 900

假设此时往下滚动了 400 px, 则滚动条滚动的百分比为 400/900 = 44.4%

对应的,此时滚动条指向的列表项称为定位项,索引值为 50 * 44.4%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# ① 确定起止项和定位项

进行滚动后,我们拿到了新的信息量:

  • 滚动偏移量(scrollTop): 通过 element.scrollTop 获得
  • 滚动条滚动百分比(scrollPtg): scrollTop / scrollTopMax
  • 滚动条偏移量(scrollBarTop): scrollBarHeightMax * scrollPtg

此时我们就可以

这里我们提到了一个定位项的概念,何为定位项?

定位项与滚动条位置对应,可以理解为滚动条水平方向指向的那个列表项。

当滚动条为0时,指向第0项,此时定位项为第0项,即本次渲染列表的 startIndex

当滚动条处于最大值时,指向最后一项,此时定位项为最后一项,即本次渲染列表的 endIndex

const scrollTopMax = listHeight - clientHeight
/** 进度条滚动百分比 */
const scrollPtg = scrollTop / scrollTopMax
/** 确定定位项 */
const itemIndex = Math.floor(scrollPtg * itemCount);
/** 可见列表项个数 = 可见容器高度 / 每个列表项高度 ,记得向上取整 */
const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight)
/** 确定起始项和结束项 */
const startIndex = Math.max(0, itemIndex - Math.ceil(scrollPtg * visibleCount))
const endIndex = Math.min(itemCount - 1, itemIndex + Math.ceil((1 - scrollPtg) * visibleCount))
const itemOffsetPtg = (scrollPtg - itemTopPtg) / (itemBottomPtg - itemTopPtg)
1
2
3
4
5
6
7
8
9
10
11

# ② 渲染列表项

渲染 startIndex ~ endIndex 的列表项

# ③ 调整 offset

在列表项渲染完毕后,触发 update 回调

获取并统计 startIndex ~ itemIndex 列表项的实际总高度 s2iHeight

计算起始项偏移高度 startItemTop ,如下:

const startItemTop = 定位项绝对高度(itemAbsoluteTop) - 起始项至定位项的高度(s2iHeight)
const itemAbsoluteTop = scrollTop + 定位项相对视口高度(itemRelativeTop)
const itemRelativeTop = 滚动过的视口高度(scrollPtg * clientHeight) - 定位项偏移高度(itemOffsetPtg * itemHeight)
1
2
3

# 健壮

由于总高度固定,不存在鼠标和滚动条不同步的问题

# 总结

性能优异,通过几个数学公式即可确定起止位置(还有优化的空间)

若需要自适应高度,则需要进行2次render,否则第一次render即可计算偏移位置

目前唯一一种不产生鼠标和滚动条不同步问题的方案

拓展性强,毕竟后面是 Ant Design 4 的核心组件之一

react 长列表首选方案

vue 可以尝试造个轮子

编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
用 CAP 理论指导 Hybrid App 离线策略优化
为什么不用rAF进行滚动节流

← 用 CAP 理论指导 Hybrid App 离线策略优化 为什么不用rAF进行滚动节流→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式