沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 必看导言

    • 就业的核心问题

      • 课件
    • 表现力的训练

      • 课件
  • 简历制作

    • 课件
    • 简历准备课堂笔记
    • 个人信息课堂笔记
    • 求职意向课堂笔记
    • 技术栈课堂笔记
    • 教育经历课堂笔记
    • 个人优势与评价课堂笔记
    • 工作经历课堂笔记
    • 项目
    • 项目亮难点问题

    • 项目经历

  • 项目准备

    • 课件
    • 课件
  • 技术重点

    • 划重点

    • 非技术环节
  • 简历投递和面试准备

    • 简历投递
    • 面试准备
  • 笔面试环节知识讲解
  • 项目准备
  • 难点攻关
  • 优化虚拟列表滚动
  • 课件资料
luzhichang
2024-09-18
目录

08-技术讲解

# 技术讲解

# 什么问题

# 1. 长列表渲染问题

问题:渲染整个长列表容易造成页面阻塞,用户体验不好

解决方案:

  • 时间分片
    • 效率低
    • 不直观
    • 性能差
  • 虚拟列表(推荐使用)

# 2. 虚拟列表原理

设置一个可视区域,然后用户在滚动列表的时候,本质上是动态修改可视区域里面的内容。

例如,一开始渲染前面 5 个项目

image-20240701162418114

之后用户进行滚动,就会动态的修改可视区域里面的内容,如下图所示:

image-20240701162813149

# 3. 虚拟列表的实现

假设列表里面每一项定高,我们需要得到一些信息:

  1. 可视区域起始数据索引(startIndex)
  2. 可视区域结束数据索引(endIndex)
  3. 可视区域的数据
  4. 整个列表中的偏移位置 startOffset

如下图所示:

image-20240701164454859

整个虚拟列表的设计如下:

<!-- 可视区域容器 -->
<div class="infinite-list-container">
  <!-- 这是容器里面的占位,高度是总列表高度,用于形成滚动条 -->
  <div class="infinite-list-phantom"></div>
  <!-- 列表项渲染区域 -->
  <div class="infinite-list">
    <!-- item-1 -->
    <!-- item-2 -->
    <!-- ...... -->
    <!-- item-n -->
  </div>
</div>
  • infinite-list-container: 可视区域容器
  • infinite-list-phantom: 这是容器里面的占位,高度是总列表高度,用于形成滚动条
  • infinite-list:列表项渲染区域

如下图所示:

image-20240701165847905

接下来监听 infinite-list-container 的 scroll 事件,获取滚动位置的 scrollTop

  • 假定可视区域高度固定,称之为 screenHeight
  • 假定列表每项高度固定,称之为 itemSize
  • 假定列表数据称之为 listData
  • 假定当前滚动位置称之为 scrollTop

那么我们能够计算出这么一些信息:

  1. 列表总高度 listHeight = listData.length * itemSize
  2. 可显示的列表项数 visibleCount = Math.ceil(screenHeight / itemSize)
  3. 数据的起始索引 startIndex = Math.floor(scrollTop / itemSize)
  4. 数据的结束索引 endIndex = startIndex + visibleCount
  5. 列表显示数据为 visibleData = listData.slice(startIndex, endIndex)

当发生滚动之后,由于渲染区域相对于可视区域发生了偏移。我们需要计算出来这个偏移量,然后使用 transform 将 list 重新移回到可视区域。

偏移量 startOffset = scrollTop - (scrollTop % itemSize)

image-20240701170126764

# 4. 遗留问题

1. 动态高度

目前的虚拟列表,是定高度 itemSize,所以很多东西很容易计算

  • 列表总高度:listHeight = listData.length * itemSize
  • 偏移量的计算:startOffset = scrollTop - (scrollTop % itemSize)
  • 数据的起始索引 startIndex = Math.floor(scrollTop / itemSize)

但是在实际应用中,很多条目并非高度相同:

image-20240702084546314

因此在这种不定高的场景下,会遇到这么一些问题:

  1. 如何获取真实高度?
  2. 相关属性的计算有何变化?
  3. 列表的渲染方式有什么改变?

2. 白屏问题

因为现在仅渲染可视区域的元素,如果用户滚动过快,会出现白屏闪烁。

# 解决思路

# 1. 动态高度

  1. 如何获取真实高度?
    • 如果能获得列表项高度数组,真实高度问题就很好解决。但在实际渲染之前是很难拿到每一项的真实高度的,所以我们采用预估一个高度渲染出真实 DOM,再根据 DOM 的实际情况去设置真实高度。
    • 创建一个缓存列表,其中列表项字段为 索引、高度与定位,并预估列表项高度用于初始化缓存列表。在渲染后根据 DOM 实际情况更新缓存列表。
  2. 相关属性该如何计算?
    • 显然以前的计算方式都无法使用了,因为那都是针对固定值设计的。
    • 于是我们需要 根据缓存列表重写计算属性、滚动回调函数,例如列表总高度的计算可以使用缓存列表最后一项的定位字段的值。
  3. 列表渲染方式有何改变?
    • 因为用于渲染页面元素的数据是根据 开始/结束索引 在 数据列表 中筛选出来的,所以只要保证索引的正确计算,那么渲染方式是无需变化的。
    • 对于开始索引,我们将原先的计算公式改为:在 缓存列表 中搜索第一个底部定位大于 列表垂直偏移量 的项并返回它的索引
    • 对于结束索引,它是根据开始索引生成的,无需修改。

# 2. 白屏闪烁

添加缓存区,整个渲染区域由 可视区 + 缓冲区 共同组成。

image-20240702090152620

# 解决细节

# 1. 动态高度

1. 预估并初始化列表

首先增加一个 props,存储预估高度

onst props = defineProps({
 	// ...
  // 预估高度
  estimatedItemSize: {
    type: Number,
    required: true
  },
 	// ...
})

父组件在使用该虚拟列表组件时,就需要传递这个 props:

<VirtualList :listData="data" :estimatedItemSize="100" v-slot="slotProps">
  <Item :item="slotProps.item" />
</VirtualList>

在虚拟列表组件里面,就需要维护一个缓存列表 postions,一开始以预估的高度来做初始化

let positions = []
// ...
const initPositions = () => {
  positions = props.listData.map((d, index) => ({
    index,
    height: props.estimatedItemSize, // 一开始以预估高度来做初始化
    top: index * props.estimatedItemSize,
    bottom: (index + 1) * props.estimatedItemSize
  }))
}

2. 更新真实数据

每次渲染之后,需要获取 DOM 的真实高度,然后去替换 postions 里面的预估高度。

这个操作放在 Vue 里面 updated 钩子里面来处理:

// 更新每一项的真实高度
const updateItemsSize = () => {
  items.value.forEach((node) => {
    let rect = node.getBoundingClientRect()
    let height = rect.height
    let index = +node.id.slice(1)
    let oldHeight = positions[index].height
    let dValue = oldHeight - height
    if (dValue) {
      positions[index].bottom -= dValue
      positions[index].height = height

      for (let k = index + 1; k < positions.length; k++) {
        positions[k].top = positions[k - 1].bottom
        positions[k].bottom -= dValue
      }
    }
  })
}

// 更新偏移量
const setStartOffset = () => {
  let startOffset = start.value >= 1 ? positions[start.value - 1].bottom : 0
  content.value.style.transform = `translate3d(0,${startOffset}px,0)`
}

onUpdated(() => {
  requestAnimationFrame(() => {
    if (!items.value || !items.value.length) {
      return
    }
    updateItemsSize()
    let height = positions[positions.length - 1].bottom
    phantom.value.style.height = height + 'px'
    setStartOffset()
  })
})

3. 重写滚动回调

滚动回调里面,主要是需要更新获取 startIndex 的方式。

遍历缓存列表 positions,找到第一个定位大于当前滚动距离 scorllTop 的条目,返回该条目的索引值即可。

//获取列表起始索引
getStartIndex(scrollTop = 0){
  let item = this.positions.find(i => i && i.bottom > scrollTop);
  return item.index;
}

const scrollEvent = () => {
  let scrollTop = list.value.scrollTop
  start.value = getStartIndex(scrollTop) // 获取 startIndex
  end.value = start.value + visibleCount.value // 根据 startIndex 获取 endIndex
  setStartOffset()
}

这里有一个优化的点。postions 是一个有序的数组,因此我们在查找的时候就可以做优化。

之前用的 find:顺序查找,时间复杂度为 O(n)

因为是有序数组,可以改为二分查找,时间复杂度为 (O(logN))

const getStartIndex = (scrollTop = 0) => {
  return binarySearch(positions, scrollTop)
}

const binarySearch = (list, value) => {
  let start = 0
  let end = list.length - 1
  let tempIndex = null
  while (start <= end) {
    let midIndex = parseInt((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
}

# 2. 白屏闪烁

通过设置缓冲区的方式来解决。

增加一个 props 叫做 bufferScale,用于接收缓冲区数据和可视区域数据的一个比例

const props = defineProps({
  // ...
  bufferScale: {
    type: Number,
    default: 1
  },
  // ...
})

接下来就可以根据这个比例,计算出上下缓冲区的数量:

// 上方缓冲区
const aboveCount = computed(() => {
  return Math.min(start.value, props.bufferScale * visibleCount.value)
})

// 下方缓冲区
const belowCount = computed(() => {
  return Math.min(props.listData.length - end.value, props.bufferScale * visibleCount.value)
})

现在 visibleData 的计算也需要更新,需要加入上下缓冲区

const visibleData = computed(() => {
  let startIdx = start.value - aboveCount.value
  let endIdx = end.value + belowCount.value
  return _listData.value.slice(startIdx, endIdx)
})

另外偏移量的计算也需要更新,需要将缓冲区考虑进去:

const setStartOffset = () => {
  let startOffset
  if (start.value >= 1) {
    let size =
      positions.value[start.value].top -
      (positions.value[start.value - aboveCount.value]
        ? positions.value[start.value - aboveCount.value].top
        : 0)
    startOffset = positions.value[start.value - 1].bottom - size
  } else {
    startOffset = 0
  }
  content.value.style.transform = `translate3d(0,${startOffset}px,0)`
}

-EOF-

Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式