公司动态

Vue2 高性能虚拟滚动组件

📅 2026/7/24 21:02:09
Vue2 高性能虚拟滚动组件
核心特性汇总固定行高、上下缓冲区各 15 条只渲染可视区域 缓冲数据translate3d GPU 硬件加速、容器will-change: transform预分配 GPU固定 DOM 池复用不频繁创建销毁 DOM仅替换文本 / 状态滚动防抖减少高频重绘回流万条大数据requestIdleCallback分批加载渲染不阻塞主线程事件委托外层统一监听点击不每条条目绑定独立 click唯一 key 行元素Vue 精准 diff 防 DOM 错乱点击跳转详情仅占位逻辑渲染字段设备名称、区域、在线状态详细功能注释标注关键优化点template !-- 虚拟列表外层容器滚动可视区域 -- div refvirtualWrap classvirtual-scroll-wrap scrollhandleScroll clickhandleListClick !-- 高度占位层撑开滚动条总高度无实际渲染内容 -- div classvirtual-phantom :style{ height: totalScrollHeight px } /div !-- 可视渲染DOM池容器translate3d GPU位移will-change预分配GPU资源 -- div refvisibleBox classvirtual-visible-box :style{ transform: translate3d(0, offsetY, 0) } !-- DOM池固定数量行复用DOM仅更新内部数据key唯一防diff错乱 -- div v-for(item, index) in renderList :keyitem.deviceId classdevice-row :data-device-iditem.deviceId div classrow-item span classlabel设备名称/span span classdevice-name{{ item.deviceName }}/span /div div classrow-item span classlabel所属区域/span span classdevice-area{{ item.area }}/span /div div classrow-item span classlabel在线状态/span span :class[online-status, item.online ? online : offline] {{ item.online ? 在线 : 离线 }} /span /div /div /div /div /template script /** * Vue2 高性能虚拟滚动组件 * 优化点DOM池复用、GPU加速、滚动防抖、事件委托、空闲时分批渲染、缓冲区15条 * 入参list 设备数组每条必须包含唯一deviceId * 行高固定ITEM_HEIGHT 80px * 上下缓冲区各15条 BUFFER_SIZE 15 */ export default { name: VirtualScroll, props: { // 原始全量设备列表万级数据 list: { type: Array, default: () [] } }, data() { return { // 配置常量统一抽离方便修改 ITEM_HEIGHT: 80, // 单条固定高度 BUFFER_SIZE: 15, // 上下缓冲区条数 // 滚动偏移量translate3d Y值 offsetY: 0, // 当前可视区域缓冲需要渲染的数据 renderList: [], // 防抖定时器标识 scrollTimer: null, // requestIdleCallback 分批渲染标记 idleHandle: null, // 缓存可视区域尺寸 wrapHeight: 0 } }, computed: { // 滚动容器总高度虚拟占位高度撑开滚动条 totalScrollHeight() { return this.list.length * this.ITEM_HEIGHT }, // 单次可视区域能展示多少条数据 visibleCount() { return Math.ceil(this.wrapHeight / this.ITEM_HEIGHT) } }, mounted() { // 初始化可视容器高度 this.initWrapSize() // 空闲时机分批处理大数据避免主线程阻塞 this.batchRenderBigData() // 窗口resize重新计算可视高度 window.addEventListener(resize, this.debounceResize) }, beforeDestroy() { // 清除所有定时器/空闲回调防止内存泄漏 clearTimeout(this.scrollTimer) if (this.idleHandle) cancelIdleCallback(this.idleHandle) window.removeEventListener(resize, this.debounceResize) }, watch: { // 原始列表更新时重新分批渲染 list: { handler() { this.batchRenderBigData() }, deep: true } }, methods: { /** * 初始化滚动容器可视高度 * 仅读取一次DOM布局属性避免滚动中频繁读取引发重排 */ initWrapSize() { const wrapDom this.$refs.virtualWrap if (!wrapDom) return this.wrapHeight wrapDom.clientHeight }, /** * 窗口resize防抖重新计算可视区域 */ debounceResize() { clearTimeout(this.scrollTimer) this.scrollTimer setTimeout(() { this.initWrapSize() this.calcRenderRange() }, 100) }, /** * 大数据分批渲染requestIdleCallback 浏览器空闲时执行 * 万条数据不一次性循环阻塞页面分片填充渲染列表 */ batchRenderBigData() { // 清除上一次未完成的空闲任务 if (this.idleHandle) cancelIdleCallback(this.idleHandle) let startIdx 0 const chunkSize 200 // 每批处理200条 const renderChunk (deadline) { // 浏览器剩余空闲时间充足则继续渲染 while (deadline.timeRemaining() 0 startIdx this.list.length) { startIdx chunkSize } // 全部加载完成后计算可视渲染范围 if (startIdx this.list.length) { this.calcRenderRange() return } // 剩余数据继续等待浏览器空闲 this.idleHandle requestIdleCallback(renderChunk) } this.idleHandle requestIdleCallback(renderChunk) }, /** * 滚动事件防抖处理减少高频重绘 * 滚动停止16ms后再计算渲染区间避免每帧执行 */ handleScroll() { clearTimeout(this.scrollTimer) this.scrollTimer setTimeout(() { this.calcRenderRange() }, 16) }, /** * 核心计算当前需要渲染的数据区间可视区域上下缓冲 * 复用DOM池只替换文本不新增删除div */ calcRenderRange() { const wrapDom this.$refs.virtualWrap if (!wrapDom) return // 获取滚动顶部偏移仅此处读取布局属性统一读取减少回流 const scrollTop wrapDom.scrollTop // 计算可视起始索引 - 上缓冲15条 let startIndex Math.floor(scrollTop / this.ITEM_HEIGHT) - this.BUFFER_SIZE startIndex Math.max(0, startIndex) // 边界保护不能小于0 // 可视结束索引 下缓冲15条 let endIndex startIndex this.visibleCount this.BUFFER_SIZE * 2 endIndex Math.min(this.list.length - 1, endIndex) // 边界保护不能超过数据总数 // 截取需要渲染的数据 this.renderList this.list.slice(startIndex, endIndex 1) // 更新translate3d偏移GPU位移不触发页面重排 this.offsetY startIndex * this.ITEM_HEIGHT }, /** * 事件委托外层容器统一监听点击不每条行绑定click节省上千监听内存 * 通过data-device-id区分点击的设备条目 */ handleListClick(e) { // 向上查找携带设备id的行DOM const targetRow e.target.closest(.device-row) if (!targetRow) return // 获取当前点击设备唯一ID const deviceId targetRow.dataset.deviceId // 占位跳转详情逻辑自行替换业务路由 this.goDeviceDetail(deviceId) }, /** * 跳转详情占位逻辑 * param {String|Number} deviceId 设备唯一ID */ goDeviceDetail(deviceId) { console.log(跳转到设备详情页设备ID, deviceId) // 示例路由逻辑占位 // this.$router.push({ path: /device/detail, query: { id: deviceId } }) } } } /script style scoped /* 滚动可视外层容器固定高度overflow滚动 */ .virtual-scroll-wrap { width: 100%; height: 600px; /* 使用时可父组件覆盖高度 */ overflow-y: auto; position: relative; } /* 滚动高度占位层仅撑开滚动条无渲染 */ .virtual-phantom { position: absolute; left: 0; top: 0; width: 100%; z-index: -1; } /* 可视DOM容器GPU加速核心配置 */ .virtual-visible-box { position: absolute; left: 0; top: 0; width: 100%; /* will-change提前通知GPU分配资源滚动不卡顿 */ will-change: transform; /* translate3d强制开启硬件加速transform仅合成层不回流重排 */ transform: translate3d(0, 0, 0); } /* 设备行固定高度DOM池复用容器 */ .device-row { height: 80px; padding: 12px 16px; border-bottom: 1px solid #eee; box-sizing: border-box; } .row-item { line-height: 26px; font-size: 14px; } .label { color: #666; margin-right: 8px; } .device-name, .device-area { color: #333; } /* 在线状态样式区分 */ .online-status { padding: 2px 8px; border-radius: 4px; } .online { background: #e6f7ff; color: #1890ff; } .offline { background: #f5f5f5; color: #999; } /style父组件使用示例template div classpage h3万级设备虚拟列表/h3 !-- 传入万条设备数据 -- VirtualScroll :listdeviceList / /div /template script import VirtualScroll from ./VirtualScroll.vue export default { components: { VirtualScroll }, data() { return { deviceList: [] } }, mounted() { // 模拟接口返回10000条设备数据 this.mockGetTenThousandData() }, methods: { mockGetTenThousandData() { const arr [] for (let i 1; i 10000; i) { arr.push({ deviceId: dev_${i}, // 全局唯一key必须存在 deviceName: 监控设备-${i}号, area: [华东一区, 华北二区, 西南三区, 华南四区][i % 4], online: i % 5 ! 0 // 模拟在线离线状态 }) } this.deviceList arr } } } /script style .page { width: 900px; margin: 30px auto; } /style