From eb9099de29180aca1e0cc4b58c6274c24012b08e Mon Sep 17 00:00:00 2001 From: xie392 Date: Mon, 3 Jun 2024 00:23:11 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E7=BC=96=E5=86=99=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messages/message-content/index.tsx | 2 + .../virtualizer-list/virtual-list.scss | 21 ++ .../virtualizer-list/virtual-list.tsx | 51 +++ src/components/virtualizer-list/virtual.ts | 355 +++--------------- 4 files changed, 122 insertions(+), 307 deletions(-) create mode 100644 src/components/virtualizer-list/virtual-list.scss create mode 100644 src/components/virtualizer-list/virtual-list.tsx diff --git a/src/components/messages/message-content/index.tsx b/src/components/messages/message-content/index.tsx index 2a526343..83036870 100644 --- a/src/components/messages/message-content/index.tsx +++ b/src/components/messages/message-content/index.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { generateMessageList } from '@/mock/data' import MessageItem from './message-item' import InfiniteScroll from '@/components/infinite-scroll' +// import VirtualList from '@/components/virtualizer-list/virtual-list' const MessageContent = () => { const parentRef = useRef(null) @@ -88,6 +89,7 @@ const MessageContent = () => { > {data.reverse().map((_, index) => renderItem(index))} + {/* */} ) } diff --git a/src/components/virtualizer-list/virtual-list.scss b/src/components/virtualizer-list/virtual-list.scss new file mode 100644 index 00000000..26250f95 --- /dev/null +++ b/src/components/virtualizer-list/virtual-list.scss @@ -0,0 +1,21 @@ +.virtual-container { + height: 600px; + width: 300px; + overflow-y: auto; + border: 1px solid #ccc; + position: relative; + } + + .virtual-content { + position: absolute; + top: 0; + left: 0; + right: 0; + } + + .virtual-item { + padding: 10px; + border-bottom: 1px solid #eee; + box-sizing: border-box; + } + \ No newline at end of file diff --git a/src/components/virtualizer-list/virtual-list.tsx b/src/components/virtualizer-list/virtual-list.tsx new file mode 100644 index 00000000..c81e7aef --- /dev/null +++ b/src/components/virtualizer-list/virtual-list.tsx @@ -0,0 +1,51 @@ +// VirtualList.tsx +import React, { useRef, useState, useEffect } from 'react' +import Virtual from './virtual' +import './virtual-list.scss' + +interface DataItem { + id: number + message: string +} + +interface VirtualListProps { + data: T[] + itemHeight?: number + height: number + children: (index: number) => React.ReactNode +} + +const VirtualList: React.FC = ({ data, itemHeight, height, children }) => { + const containerRef = useRef(null) + const [visibleData, setVisibleData] = useState([]) + + useEffect(() => { + if (containerRef.current) { + const virtual = new Virtual(containerRef.current, { + height, + itemHeight, + data, + renderItem: () => <>111 + }) + setVisibleData(virtual.visibleData) + } + // return () => { + // virtual.destroy() + // } + }, [containerRef, data, itemHeight, height, children]) + // = new Virtual(containerRef.current, {}) + + return ( +
+ {visibleData.map((item, index) => { + return ( +
+ {children(index)} +
+ ) + })} +
+ ) +} + +export default VirtualList diff --git a/src/components/virtualizer-list/virtual.ts b/src/components/virtualizer-list/virtual.ts index c863b926..310c657b 100644 --- a/src/components/virtualizer-list/virtual.ts +++ b/src/components/virtualizer-list/virtual.ts @@ -1,322 +1,63 @@ -/** - * @file 虚拟滚动核心逻辑文件 - (构造函数) - * @description 主要用于虚拟滚动计算和管理 - * @param {Object} param 定制参数 - * @param {Function} callUpdate 回调 - */ -// @ts-nocheck -export default class Virtual { - sizes: Map = new Map() // 存储元素的尺寸的Map对象 - offset: number - - constructor(param, callUpdate) { - this.init(param, callUpdate) - } - - /** - * 初始化 - * @param {object} param - 参数对象 - * @param {function} callUpdate - 更新回调函数 - */ - init(param, callUpdate) { - this.param = param - this.callUpdate = callUpdate - this.sizes = new Map() - this.firstRangeTotalSize = 0 - this.firstRangeAverageSize = 0 - this.lastCalcIndex = 0 - this.fixedSizeValue = 0 - this.calcType = 'INIT' - - this.offset = 0 - this.direction = '' - - this.range = Object.create(null) - if (param) { - this.checkRange(0, param.keeps - 1) - } - } - - // 销毁整个虚拟列表实例 - destroy() { - this.init(null, null) - } - - /** - * 获取当前范围 - * @returns {object} - 当前范围对象 - */ - getRange() { - const range = Object.create(null) - range.start = this.range.start - range.end = this.range.end - range.padFront = this.range.padFront - range.padBehind = this.range.padBehind - return range - } - - isBehind() { - return this.direction === 'BEHIND' - } - - isFront() { - return this.direction === 'FRONT' - } - - getOffset(start) { - return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize - } - - // 更新参数 - updateParam(key, value) { - if (this.param && key in this.param) { - if (key === 'uniqueIds') { - this.sizes.forEach((v, key) => { - if (!value.includes(key)) { - this.sizes.delete(key) - } - }) - } - this.param[key] = value - } - } - - /** - * 保存元素的尺寸 - * @param {string} id - 唯一标识 - * @param {number} size - 尺寸 - * @description 该方法用于保存数据项的尺寸信息。将指定id和尺寸保存到`sizes`映射中,以便后续使用 - */ - saveSize(id, size) { - this.sizes.set(id, size) - - // 如果当前计算类型为初始化(INIT) 将尺寸值设为固定尺寸值,并将计算类型设置为固定 - if (this.calcType === 'INIT') { - this.fixedSizeValue = size - this.calcType = 'FIXED' - // 如果当前计算类型固定并且固定尺寸值不等于新的尺寸 - } else if (this.calcType === 'FIXED' && this.fixedSizeValue !== size) { - this.calcType = 'DYNAMIC' - delete this.fixedSizeValue // 删除固定尺寸值 - } - - if (this.calcType !== 'FIXED' && typeof this.firstRangeTotalSize !== 'undefined') { - // 如果sizes映射的大小小于参数keeps和uniqueIds长度的最小值 这么做的目的是为了计算平均尺寸 - if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) { - // 第一个范围的总尺 - this.firstRangeTotalSize = [...this.sizes.values()].reduce( - (acc, val) => acc + val, - 0 - ) - // 第一个范围的平均尺寸 - this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size) - } else { - delete this.firstRangeTotalSize - } - } - } - - /** - * 数据变化处理 - */ - handleDataSourcesChange() { - let start = this.range.start - - if (this.isFront()) { - // 将起始位置向前调整2个单位 - start = start - 2 - } else if (this.isBehind()) { - // 将起始位置向后调整2个单位 - start = start + 2 - } - - start = Math.max(start, 0) // 确保起始位置不小于0 - - this.updateRange(this.range.start, this.getEndByStart(start)) - } - - handleSlotSizeChange() { - this.handleDataSourcesChange() - } - - /** - * 滚动处理 - * @param offset 偏移量 - * @description 根据滚动的偏移量判断滚动的方向是向前还是向后 - */ - handleScroll(offset) { - this.direction = offset < this.offset ? 'FRONT' : 'BEHIND' - this.offset = offset - - if (!this.param) { - return - } - - if (this.direction === 'FRONT') { - this.handleFront() - } else if (this.direction === 'BEHIND') { - this.handleBehind() - } - } - - /** - * 向前滚动处理 - * @returns {number} 滚动的位置 - */ - handleFront() { - const overs = this.getScrollOvers() - if (overs > this.range.start) { - return - } - - const start = Math.max(overs - this.param.buffer, 0) - this.checkRange(start, this.getEndByStart(start)) - } - - /** - * 向后滚动处理 - * @returns {number} 滚动的位置 - */ - handleBehind() { - const overs = this.getScrollOvers() - if (overs < this.range.start + this.param.buffer) { - return - } - - this.checkRange(overs, this.getEndByStart(overs)) - } - - /** - * 获取滚动的位置 - * @returns {number} 滚动的位置 - */ - getScrollOvers() { - const offset = this.offset - this.param.slotHeaderSize - if (offset <= 0) { - return 0 - } - - if (this.isFixedType()) { - return Math.floor(offset / this.fixedSizeValue) - } - - let low = 0 - let middle = 0 - let middleOffset = 0 - let high = this.param.uniqueIds.length - - while (low <= high) { - middle = low + Math.floor((high - low) / 2) - middleOffset = this.getIndexOffset(middle) - - if (middleOffset === offset) { - return middle - } else if (middleOffset < offset) { - low = middle + 1 - } else if (middleOffset > offset) { - high = middle - 1 - } - } - - return low > 0 ? --low : 0 - } - - /** - * 获取指定索引的偏移量 - * @param {number} givenIndex - 给定的索引 - * @returns {number} - 偏移量 - */ - getIndexOffset(givenIndex) { - if (!givenIndex) { - return 0 - } - - let offset = 0 - let indexSize = 0 - for (let index = 0; index < givenIndex; index++) { - indexSize = this.sizes.get(this.param.uniqueIds[index]) - offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize()) - } - - this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1) - this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex()) - - return offset - } - - isFixedType() { - return this.calcType === 'FIXED' - } - - getLastIndex() { - return this.param.uniqueIds.length - 1 - } - - checkRange(start, end) { - const keeps = this.param.keeps - const total = this.param.uniqueIds.length - - if (total <= keeps) { - start = 0 - end = this.getLastIndex() - } else if (end - start < keeps - 1) { - start = end - keeps + 1 - } +export interface VirtualProps { + data: T[] + itemHeight?: number + height: number + onScroll?: (startIndex: number, endIndex: number) => void + renderItem: (item: T, index: number) => JSX.Element + onResize?: (height: number) => void +} +class Virtual { + data: T[] + visibleData: T[] + itemHeight: number + height: number + container: HTMLDivElement + options: VirtualProps - if (this.range.start !== start) { - this.updateRange(start, end) - } + constructor(el: HTMLDivElement, options: VirtualProps) { + this.data = options.data + this.itemHeight = options.itemHeight ?? 50 + this.visibleData = [] + this.height = options.height + this.container = el + this.options = options + this.container.addEventListener('scroll', this.handlerScroll.bind(this)) + this.initContainerStyle() + this.calculateVisibleData() } - // 更新范围 - updateRange(start, end) { - this.range.start = start - this.range.end = end - this.range.padFront = this.getPadFront() - this.range.padBehind = this.getPadBehind() - this.callUpdate(this.getRange()) + initContainerStyle() { + this.container.style.overflowY = 'auto' + this.container.style.height = `${this.height}px` } - getEndByStart(start) { - const theoryEnd = start + this.param.keeps - 1 - const truelyEnd = Math.min(theoryEnd, this.getLastIndex()) - return truelyEnd + calculateVisibleIndices() { + const startIndex = Math.floor(this.container.scrollTop / this.itemHeight) + const endIndex = Math.ceil((this.container.scrollTop + this.height) / this.itemHeight) + return { startIndex, endIndex } } - // 获取前方的预填充大小 - getPadFront() { - if (this.isFixedType()) { - return this.fixedSizeValue * this.range.start - } else { - return this.getIndexOffset(this.range.start) - } + calculateVisibleData() { + const { startIndex, endIndex } = this.calculateVisibleIndices() + this.visibleData = this.data.slice(startIndex, endIndex) } - /** - * 获取后方的预填充大小 - * @returns {number} - 填充大小 - */ - getPadBehind() { - const end = this.range.end // 当前显示范围的结束索引 - const lastIndex = this.getLastIndex() // 最后一个索引 - - // 如果是固定尺寸类型就直接返回固定尺寸的填充大小 - if (this.isFixedType()) { - return (lastIndex - end) * this.fixedSizeValue + handlerScroll() { + const { startIndex, endIndex } = this.calculateVisibleIndices() + this.visibleData = this.data.slice(startIndex, endIndex) + if (this.options.onScroll) { + this.options.onScroll(startIndex, endIndex) } + console.log('startIndex: ', startIndex, 'endIndex: ', endIndex) - // 如果上一次计算的索引等于最后一个索引,则返回当前索引的偏移量减去结束索引的偏移量 - if (this.lastCalcIndex === lastIndex) { - return this.getIndexOffset(lastIndex) - this.getIndexOffset(end) - } else { - // 否则返回预估尺寸乘以剩余元素个数的填充尺寸 - return (lastIndex - end) * this.getEstimateSize() - } + this.render() } - getEstimateSize() { - return this.isFixedType() - ? this.fixedSizeValue - : this.firstRangeAverageSize || this.param.estimateSize + render() { + // remove all children + // render visible data with renderItem + console.log('render') } } + +export default Virtual