当前位置: 首页 > news >正文

虚拟滚动优化——js技能提升

5. 虚拟滚动优化

功能概述

实现高性能的虚拟滚动系统,支持动态高度计算、滚动位置管理和内存优化。

技术难点

  • 虚拟滚动算法实现
  • 动态高度计算
  • 滚动位置管理
  • 性能优化和内存管理
  • 用户体验优化

实现思路

5.1 虚拟滚动核心组件
// src/pages/chat/components/ChatList/index.tsx
import {defineComponent,ref,type ComponentPublicInstance,onMounted,onUnmounted,watch,type ExtractPropTypes,type PropType,reactive,nextTick,
} from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
import { NScrollbar } from 'naive-ui'
import { Bubble } from '@/components'
import cls from 'classnames'
import { NotificationType } from '@/types'export default defineComponent({name: 'ChatList',props: {messages: {type: Array as PropType<ExtractPropTypes<typeof Bubble>['content'][]>,required: true,default: () => [],},contentClass: {type: String,default: '',},},setup(props) {const scrollRef = ref<HTMLElement | null>(null)const state = reactive({ isBottom: true, totalSize: 0 })// 虚拟滚动配置const virtualizer = useVirtualizer({count: props.messages.length,getScrollElement: () => scrollRef.value,estimateSize: () => 100, // 预估每个项目的高度overscan: 5, // 预渲染的项目数量measureElement: el => el.getBoundingClientRect().height, // 动态测量元素高度})onMounted(() => {// 获取滚动容器元素scrollRef.value = document.querySelector('.chat-scrollbar .n-scrollbar-container')scrollRef.value?.addEventListener('wheel', wheelCB)})onUnmounted(() => {scrollRef.value?.removeEventListener('wheel', wheelCB)})// 监听消息列表变化watch(() => props.messages,(list, oldList) => {// 判断是否在底部state.isBottom = state.isBottom ? true : list.length > oldList.length// 更新虚拟滚动配置virtualizer.value.setOptions({...virtualizer.value.options,count: list.length,})})// 监听虚拟项目变化watch(() => virtualizer.value.getVirtualItems(),() => updateVirtualItems())// 更新虚拟项目const updateVirtualItems = async () => {if (!scrollRef.value) return// 计算总高度state.totalSize = Number([...scrollRef.value!.querySelectorAll('.chat-item')].reduce((acc, item) => acc + item.getBoundingClientRect().height, 0).toFixed(0))// 如果在底部,自动滚动到底部if (!state.isBottom) returnawait nextTick()scrollRef.value!.scrollTop = Number.MAX_SAFE_INTEGER}// 滚轮事件处理const wheelCB = (e: WheelEvent) => {// 向上滚动时,标记不在底部state.isBottom = e.deltaY < 0 && false}// 虚拟项目回调const virtualItemCB = (el: Element | ComponentPublicInstance | null) => {if (!(el instanceof HTMLElement)) returnvirtualizer.value.measureElement(el)}return () => {return (<NScrollbarclass="chat-scrollbar w-full flex-1 px-5 opacity-100"contentClass={cls('relative !min-w-[540px]', props.contentClass)}contentStyle={{ height: `${state.totalSize}px` }}>{virtualizer.value.getVirtualItems().map(({ index, start }) => {const item = props.messages[index]if (!item) return nullreturn (<divkey={String(index)}data-index={index}ref={virtualItemCB}class={cls('chat-item absolute top-0 left-0 w-full pb-4', {hidden:index === props.messages.length - 1 &&item.type !== NotificationType.QUESTION,})}style={{ transform: `translateY(${start}px)` }}><Bubble content={item} /></div>)})}{/* 渲染最后一条消息的特殊处理 */}{(() => {const lastChat = props.messages[props.messages.length - 1]if (lastChat && lastChat.type === NotificationType.QUESTION) {return (<divclass="chat-item relative w-full pb-4"style={{transform: `translateY(${virtualizer.value.getTotalSize()}px)`,}}><Bubble content={lastChat} /></div>)}return null})()}</NScrollbar>)}},
})
5.2 无限滚动组件
// src/pages/chat/knowledge-base/index.tsx
import { defineComponent, Fragment, reactive } from 'vue'
import { definePageMeta, useHead } from '#imports'
import { NDivider, NInput, NSelect, NInfiniteScroll } from 'naive-ui'
import { SearchIcon } from '@/assets/icons'
import { type KnowledgeItem } from '@/utils'
import { Upload, Item } from './components'
import cls from 'classnames'
import { common } from '@/theme'const tabList = [{ label: '知识库', value: 'knowledge' }]export default defineComponent({name: 'KnowledgeBase',setup() {definePageMeta({ layout: 'chat' })useHead({title: 'JetPave',meta: [{ name: 'description', content: 'JetPave SaaS' }],htmlAttrs: { lang: 'zh-CN' },})const state = reactive({fileList: [] as KnowledgeItem[],type: 0,loading: true,noMore: false,page: 1,pageSize: 20,})// 加载更多数据const loadMore = async () => {if (state.loading || state.noMore) returnstate.loading = truetry {// 模拟API调用await new Promise(resolve => setTimeout(resolve, 1000))// 模拟数据const newItems = Array.from({ length: state.pageSize }, (_, i) => ({id: state.page * state.pageSize + i,name: `文件${state.page * state.pageSize + i}`,type: 'application/pdf',size: '1.2MB',}))// 检查是否还有更多数据if (newItems.length < state.pageSize) {state.noMore = true}state.fileList.push(...newItems)state.page++} catch (error) {console.error('加载数据失败:', error)} finally {state.loading = false}}return () => (<div class="flex-1 flex flex-col gap-4 overflow-hidden"><div class="flex justify-between items-center px-4"><div class="flex">{tabList.map((i, index) => (<Fragment key={i.value}><divclass={cls('transition', {[common.textPrimary]: state.type === index,})}>{i.label}</div>{index !== tabList.length - 1 && <NDivider vertical />}</Fragment>))}</div><div class="flex items-center gap-4"><NInputclass="!w-56 shrink-0"placeholder="搜索"v-slots={{ prefix: () => <SearchIcon /> }}/><NSelectclass="w-24 shrink-0"options={[{ label: '全部', value: 'all' }]}defaultValue="all"/><Upload /></div></div><NInfiniteScrollclass="flex-1"scrollbarProps={{contentClass: 'py-4',}}onLoad={loadMore}><div class="flex flex-wrap gap-5 mx-auto max-w-[808px] xl:max-w-[1084px]">{state.fileList.map((item, i) => (<Item key={item.id} data={item} />))}</div>{state.loading && (<div class="text-center p-4"><div class="inline-flex items-center gap-2"><div class="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div><span class="text-gray-500">加载中...</span></div></div>)}{state.noMore && (<div class="text-center p-4 text-gray-500">没有更多了 🤪</div>)}</NInfiniteScroll></div>)},
})
5.3 缩略图堆叠组件
// src/pages/chat/components/ThumbnailStack/index.tsx
import { defineComponent, ref, watch, computed } from 'vue'
import cls from 'classnames'
import { mergeCss } from '@/theme'export const ThumbnailStack = defineComponent({name: 'ThumbnailStack',props: {activeIndex: {type: Number,default: 0,},thumbnails: {type: Array as PropType<any[]>,default: () => [],},},emits: ['update:activeIndex'],setup(props, { emit }) {const localActiveIndex = ref(props.activeIndex)// 更新活动索引const updateActiveIndex = (newIndex: number) => {localActiveIndex.value = newIndexemit('update:activeIndex', newIndex)}// 监听props变化watch(() => props.activeIndex,newIndex => {localActiveIndex.value = newIndex})// 监听缩略图变化watch(() => props.thumbnails,newThumbnails => {if (newThumbnails.length > 0) {newThumbnails.forEach((item, index) => {// 处理缩略图数据})}},{ deep: true, immediate: true })// 计算每个卡片的3D样式const getCardStyle = (index: number) => {const relativeIndex =(index - localActiveIndex.value + props.thumbnails.length) % props.thumbnails.lengthconst absCoord = Math.abs(relativeIndex)// 只显示前5张卡片if (absCoord > 4) {return { display: 'none' }}const zIndex = 100 - absCoord// 垂直前后层叠效果参数let scale = 1let translateX = 0let translateY = 0let translateZ = 0let rotateX = 0let opacity = 1let width = '80%'let height = '80%'let top = '50%'if (absCoord === 0) {scale = 1translateX = 0translateY = 0translateZ = 0rotateX = 0opacity = 1top = '47%'} else if (absCoord === 1) {scale = 0.95translateX = 0translateY = 0translateZ = -20rotateX = 0opacity = 0.75top = '40%'} else if (absCoord === 2) {scale = 0.9translateX = 0translateY = 0translateZ = -40rotateX = 0opacity = 0.55top = '36%'} else if (absCoord === 3) {scale = 0.85translateX = 0translateY = 0translateZ = -60rotateX = 0opacity = 0.35top = '38%'} else if (absCoord === 4) {scale = 0.8translateX = 0translateY = 0translateZ = -80rotateX = 0opacity = 0.2top = '36%'}return {zIndex,opacity,width,height,left: '50%',top,transform: `translate(-50%, -50%) perspective(800px) translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) scale(${scale})`,transformOrigin: 'center center',}}// 切换到指定索引const goToSlide = (index: number) => {updateActiveIndex(index)}// 切换到下一张const nextSlide = () => {const nextIndex = (localActiveIndex.value + 1) % props.thumbnails.lengthupdateActiveIndex(nextIndex)}// 切换到上一张const prevSlide = () => {const prevIndex = (localActiveIndex.value - 1 + props.thumbnails.length) % props.thumbnails.lengthupdateActiveIndex(prevIndex)}return () => (<div class="relative w-full h-full overflow-hidden">{/* 缩略图容器 */}<div class="relative w-full h-full">{props.thumbnails.map((thumbnail, index) => (<divkey={index}class={cls('absolute cursor-pointer transition-all duration-500 ease-out',mergeCss(['transition']))}style={getCardStyle(index)}onClick={() => goToSlide(index)}><div class="w-full h-full rounded-lg overflow-hidden shadow-lg"><imgsrc={thumbnail.url}alt={thumbnail.title}class="w-full h-full object-cover"/></div></div>))}</div>{/* 导航按钮 */}<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2"><buttononClick={prevSlide}class={cls('w-8 h-8 rounded-full bg-white/80 hover:bg-white','flex items-center justify-center','transition-all duration-200',mergeCss(['transition']))}><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg></button><div class="flex items-center gap-1">{props.thumbnails.map((_, index) => (<buttonkey={index}onClick={() => goToSlide(index)}class={cls('w-2 h-2 rounded-full transition-all duration-200',{'bg-white': index === localActiveIndex.value,'bg-white/40': index !== localActiveIndex.value,})}/>))}</div><buttononClick={nextSlide}class={cls('w-8 h-8 rounded-full bg-white/80 hover:bg-white','flex items-center justify-center','transition-all duration-200',mergeCss(['transition']))}><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg></button></div></div>)},
})
5.4 技能列表组件
// src/components/SendWidget/components/SkillList/index.tsx
import { defineComponent, reactive } from 'vue'
import { NInput, NScrollbar } from 'naive-ui'
import cls from 'classnames'
import { mergeCss } from '@/theme'const utilList = [{name: '数据分析',desc: '智能分析数据趋势和模式',icon: '📊',color: 'bg-blue-500',},{name: '代码生成',desc: '根据需求生成高质量代码',icon: '💻',color: 'bg-green-500',},{name: '文档写作',desc: '协助撰写各类文档和报告',icon: '📝',color: 'bg-purple-500',},{name: '图像处理',desc: '智能图像编辑和优化',icon: '🖼️',color: 'bg-orange-500',},{name: '翻译助手',desc: '多语言翻译和本地化',icon: '🌐',color: 'bg-red-500',},
]export default defineComponent({name: 'SkillList',props: {show: {type: Boolean,default: false,},},setup(props, { emit }) {const state = reactive({searchUtil: '',})const onClose = () => emit('update:show', false)return () => (<div class="py-2"><div class="px-2 pb-2"><NInputplaceholder="搜索技能"class="!rounded-lg"size="large"v-model:value={state.searchUtil}/></div><NScrollbar class="h-[200px]" trigger="none">{utilList.filter(item =>state.searchUtil ? item.name.includes(state.searchUtil) : true).map(item => (<divclass={cls('flex items-center gap-3 mx-2 px-2 py-2 rounded-lg cursor-pointer',mergeCss(['bgHover', 'transition']))}onClick={() => {state.searchUtil = item.nameonClose()}}><divclass={cls('w-6 h-6 rounded flex items-center justify-center',item.color)}><span class="text-sm">{item.icon}</span></div><div class="flex-1"><div class="font-medium">{item.name}</div><div class="text-sm line-clamp-1">{item.desc}</div></div></div>))}</NScrollbar></div>)},
})

关键技术点

5.1 虚拟滚动算法
  1. 视口计算: 只渲染可见区域内的元素
  2. 高度预估: 使用预估高度进行初始布局
  3. 动态测量: 实时测量实际元素高度
  4. 预渲染: 渲染超出视口的元素以提供平滑滚动
5.2 性能优化策略
  1. 内存管理: 及时清理不可见的DOM元素
  2. 事件优化: 使用事件委托减少事件监听器
  3. 渲染优化: 避免不必要的重渲染
  4. 滚动优化: 使用transform代替top/left定位
5.3 用户体验优化
  1. 平滑滚动: 使用CSS transition提供流畅动画
  2. 加载状态: 显示加载指示器
  3. 错误处理: 优雅处理加载失败情况
  4. 响应式设计: 适配不同屏幕尺寸
5.4 无限滚动实现
  1. 分页加载: 按需加载数据
  2. 状态管理: 管理加载状态和是否还有更多数据
  3. 防抖处理: 避免频繁触发加载
  4. 错误重试: 自动重试失败的请求

总结

虚拟滚动是现代前端应用中处理大量数据的重要技术,通过只渲染可见元素来显著提升性能。实现要点包括:

  1. 算法设计: 精确计算可见区域和需要渲染的元素
  2. 性能优化: 减少DOM操作和内存占用
  3. 用户体验: 提供流畅的滚动体验和加载反馈
  4. 错误处理: 优雅处理各种异常情况

这5个功能模块展现了现代前端开发的技术深度,每个功能都需要深入理解相关技术原理和最佳实践才能实现。对于想要提升技术水平的开发者来说,这些功能都是很好的学习案例。

http://www.dtcms.com/a/351185.html

相关文章:

  • zookeeper-保姆级配置说明
  • http与https配置
  • 使用分流电阻器时的注意事项--PCB 设计对电阻温度系数的影响
  • Ubuntu 虚拟机配置 Git 并推送到Gitee
  • 低代码如何颠覆企业系统集成传统模式?快来一探究竟!
  • 两数之和,leetCode热题100,C++实现
  • 2025年视觉、先进成像和计算机技术论坛(VAICT 2025)
  • LeetCode热题100--108. 将有序数组转换为二叉搜索树--简单
  • 【Lua】题目小练11
  • Ansible 自动化运维工具:介绍与完整部署(RHEL 9)
  • 【软考论文】论领域驱动开发方法(DDD)的应用
  • CentOS 7服务器初始化全攻略:从基础配置到安全加固
  • AI应用--接口测试篇
  • Maya绑定基础:驱动关键帧的使用
  • C# .NET支持多线程并发的压缩组件
  • 视频创作者如何用高级数据分析功能精准优化视频策略
  • 红色文化与前沿科技的融合:VR呈现飞夺泸定桥的震撼历史场景​
  • LWIP协议栈
  • Java项目-苍穹外卖_Day3-Day4
  • MyBatis-Flex:一个支持关联查询的MyBatis
  • android vehicle
  • SOME/IP-SD协议含配置选项键值信息的报文示例解析
  • 贝叶斯优化提升化学合成反应效率(附源码)
  • 如何将数据从vivo手机传输到另一部vivo手机
  • 《高并发场景下数据一致性隐疾的实战复盘》
  • Coze Studio开源版:AI Agent开发平台的深度技术解析- 入门篇
  • 深度学习篇---LeNet-5网络结构
  • iOS 开发中的 UIStackView 使用详解
  • Linux-服务器初始化
  • RHEL8.6环境下批量验证服务器凭据并配置Ansible免密管理全流程