UNPKG

@pixui-dev/pixui-react-virtualwaterfall

Version:

pixui 高性能React虚拟瀑布流组件

1,156 lines (1,155 loc) 50.2 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { h, Component, createRef } from 'preact'; /** * 瀑布流单项组件,负责渲染单个项目 * @class WaterfallItemComponent * @extends {Component<WaterfallItemComponentProps, WaterfallItemComponentState>} */ var WaterfallItemComponent = /** @class */ (function (_super) { __extends(WaterfallItemComponent, _super); /** * 构造函数 * @param {WaterfallItemComponentProps} props - 组件属性 */ function WaterfallItemComponent(props) { var _this = _super.call(this, props) || this; _this.state = { x: props.x, y: props.y, width: props.width, height: props.height, element: props.element }; return _this; } /** * 更新项目的位置和尺寸 * @param {WaterfallItemComponentProps} props - 新的属性值 */ WaterfallItemComponent.prototype.updateState = function (props) { this.setState({ x: props.x, y: props.y, width: props.width, height: props.height }); }; /** * 更新项目的元素内容 * @param {JSX.Element} element - 新的元素 */ WaterfallItemComponent.prototype.updateElement = function (element) { this.setState({ element: element }); }; /** * 渲染组件 * @returns {JSX.Element} 渲染的JSX元素 */ WaterfallItemComponent.prototype.render = function () { var _a = this.state, x = _a.x, y = _a.y, width = _a.width, height = _a.height, element = _a.element; return (h("div", { className: 'waterfall-item', style: { position: 'absolute', left: "".concat(x, "px"), top: "".concat(y, "px"), width: "".concat(width, "px"), height: "".concat(height, "px"), } }, element)); }; return WaterfallItemComponent; }(Component)); /** * 虚拟瀑布流列表组件 * @class VirtualWaterfall * @extends {Component<VirtualWaterfallProps, VirtualWaterfallState>} * @description 高性能的虚拟瀑布流列表实现,支持大量数据的流畅滚动和元素复用 */ var VirtualWaterfall = /** @class */ (function (_super) { __extends(VirtualWaterfall, _super); /** * 构造函数 * @param {VirtualWaterfallProps} props - 组件属性 */ function VirtualWaterfall(props) { var _this = _super.call(this, props) || this; /** =================== DOM 引用 =================== */ /** * 容器DOM引用 * @private * @type {Ref<HTMLDivElement>} */ _this.containerRef = createRef(); /** * 内容DOM引用 * @private * @type {Ref<HTMLDivElement>} */ _this.contentRef = createRef(); /** * 加载更多DOM引用 * @private * @type {Ref<HTMLDivElement>} */ _this.loadMoreRef = createRef(); /** =================== 数据相关 =================== */ /** * 数据 * @public * @type {any[]} */ _this.renderData = []; /** =================== 布局状态 =================== */ /** * 各列的当前高度 * @private * @type {number[]} */ _this.columnHeights = []; /** * 总内容高度 * @private * @type {number} */ _this.totalHeight = 0; /** * 缓存的所有项目布局信息 * @private * @type {CachedItemLayout[]} */ _this.cachedLayouts = []; /** * 已计算布局的项目数量 * @private * @type {number} */ _this.layoutedItemCount = 0; /** =================== 可视化状态 =================== */ /** * 当前可见的瀑布流项目列表 * @private * @type {WaterfallItem[]} */ _this.waterfallItems = []; /** * 索引到瀑布流项目的映射表 * @private * @type {Map<number, WaterfallItem>} */ _this.waterfallItemsMap = new Map(); /** * 上次可见项目的索引列表,用于对比变化 * @private * @type {number[]} */ _this.lastVisibleIndices = []; /** * 当前滚动位置(不存储在state中避免触发重渲染) * @private * @type {number} */ _this.currentScrollTop = 0; /** * 可视区域起始索引 * @private * @type {number} */ _this.visibleStartIndex = 0; /** * 可视区域结束索引 * @private * @type {number} */ _this.visibleEndIndex = 0; /** =================== 下拉刷新相关 =================== */ /** * 是否启用下拉刷新 * @private * @type {() => boolean} */ _this.hasPullDownRefresh = function () { return false; }; /** * 下拉刷新状态 * @private * @type {'idle' | 'pulling' | 'ready' | 'refreshing'} */ _this.pullDownRefreshState = 'idle'; /** * 下拉刷新距离 * @private * @type {number} */ _this.pullDownRefreshDistance = 0; /** * 下拉刷新的最小距离阈值 * @private * @type {number} */ _this.pullDownRefreshThreshold = VirtualWaterfall.defaultProps.pullDownRefreshThreshold; /** =================== 加载更多相关 =================== */ /** * 是否正在加载更多 * @private * @type {boolean} */ _this.isLoadingMore = false; /** =================== 缓存复用 =================== */ /** * 按类型分组的回收元素缓存池 * @private * @type {Map<string, WaterfallItem[]>} */ _this.recycledWaterfallItemsByType = new Map(); /** * 生成唯一key的计数器 * @private * @type {number} */ _this.keyIndex = 0; /** =================== 事件处理方法 =================== */ /** * 滚动事件处理函数 * @private * @description 处理滚动事件,更新可视区域和触发加载更多 */ _this.handleScroll = function () { if (!_this.containerRef.current) return; // 获取滚动位置 var scrollTop = _this.containerRef.current.scrollTop; _this.currentScrollTop = scrollTop; // console.log('handleScroll', scrollTop); // 检查下拉刷新状态 _this.checkAndTriggerPullDownRefresh(scrollTop); // 检查是否需要触发加载更多数据 _this.checkAndTriggerLoadMore(scrollTop); // 计算可视区域变化并更新渲染 var hasVisibleChange = _this.calculateVisibleItems(); if (hasVisibleChange) { _this.forceUpdate(); } }; /** =================== 缓存管理方法 =================== */ /** * 重置所有缓存状态 * @private * @description 清空所有缓存数据,重置组件状态 */ _this.resetCache = function () { console.log('resetCache'); // 清理布局缓存 _this.cachedLayouts = []; _this.layoutedItemCount = 0; _this.columnHeights = new Array(_this.props.columns).fill(0); _this.totalHeight = 0; // 清理可视区域缓存 _this.lastVisibleIndices = []; // 清理瀑布流项目缓存 _this.waterfallItems = []; _this.waterfallItemsMap.clear(); // 清理可视区域索引 _this.visibleStartIndex = 0; _this.visibleEndIndex = 0; // 清理缓存池 _this.recycledWaterfallItemsByType.clear(); // 清理索引缓存 _this.keyIndex = 0; }; /** * 获取项目类型(用于分类缓存) * @private * @param {any} item - 数据项 * @returns {string} 项目类型 * @description 如果用户提供了getItemType函数则使用,否则返回默认类型 */ _this.getItemType = function (item) { var getItemType = _this.props.getItemType; return getItemType ? getItemType(item) : 'default'; }; /** * 将项目回收到缓存池中 * @private * @param {WaterfallItem} waterfallItem - 要回收的瀑布流项目 * @description 将不可见的项目回收到对应类型的缓存池中以供复用 */ _this.recycleElement = function (waterfallItem) { var itemType = waterfallItem.itemType; // 确保该类型的缓存数组存在 if (!_this.recycledWaterfallItemsByType.has(itemType)) { _this.recycledWaterfallItemsByType.set(itemType, []); } var cachedWaterfallItems = _this.recycledWaterfallItemsByType.get(itemType); // 限制缓存池大小,避免内存泄漏 var MAX_CACHE_SIZE = 20; if (cachedWaterfallItems.length < MAX_CACHE_SIZE) { var cachedVNodeRef = waterfallItem.vNode.ref; if (cachedVNodeRef.current) { // 移动到屏幕外 cachedVNodeRef.current.updateState({ x: -10000, y: -10000, width: 0, height: 0, }); } cachedWaterfallItems.push(waterfallItem); } }; // =================== 布局计算方法 =================== /** * 计算指定范围内项目的布局信息 * @private * @param {number} startIndex - 开始索引 * @param {number} targetCount - 目标数量,即需要计算的项目的数量 * @description 使用瀑布流算法计算每个项目的位置和尺寸 */ _this.layoutItems = function (startIndex, targetCount) { var _a = _this.props, columns = _a.columns, itemWidth = _a.itemWidth, _b = _a.gap, gap = _b === void 0 ? VirtualWaterfall.defaultProps.gap : _b; var data = _this.renderData; var endIndex = Math.min(startIndex + targetCount, data.length); // console.log('layoutItems', startIndex, endIndex); if (targetCount === 0) { return; } // 恢复布局, 如果 startIndex 不为 0,则使用 cachedLayouts 中的缓存布局 var columnHeights = _this.columnHeights; if (startIndex !== 0) { // 使用缓存布局中的各列高度,用于恢复布局,slice 是为了避免引用问题 columnHeights = _this.cachedLayouts[startIndex - 1].cachedColumnHeights.slice(); } else { // 如果 startIndex 为 0,则清理布局缓存 _this.cachedLayouts = []; _this.layoutedItemCount = 0; columnHeights = new Array(columns).fill(0); } // 计算每个项目的布局 for (var i = startIndex; i < endIndex; i++) { var item = data[i]; // 找到当前最短的列 var shortestColumnIndex = columnHeights.indexOf(Math.min.apply(Math, columnHeights)); // 获取项目高度 var height = _this.props.getItemHeight(item, i, itemWidth); // 计算项目位置 var x = shortestColumnIndex * (itemWidth + gap); var y = columnHeights[shortestColumnIndex]; // 更新该列的高度 columnHeights[shortestColumnIndex] += height + gap; // 缓存布局信息 var layout = { x: x, y: y, width: itemWidth, height: height, columnIndex: shortestColumnIndex, index: i, // 缓存各列的高度,用于恢复布局 cachedColumnHeights: columnHeights.slice(), }; _this.cachedLayouts[i] = layout; // console.log('layoutItems', i, layout); } // console.log('layoutItems', this.cachedLayouts); // console.log('columnHeights', columnHeights); // 更新布局状态 _this.layoutedItemCount = endIndex; if (data.length == 0) { _this.totalHeight = 0; } else { // 获取最后一个项目的缓存列高度 var cachedColumnHeights = _this.cachedLayouts[data.length - 1].cachedColumnHeights; // 更新总高度 _this.totalHeight = Math.max.apply(Math, cachedColumnHeights); } // console.log('cachedColumnHeights', this.cachedLayouts[data.length - 1], cachedColumnHeights); // console.log('totalHeight', this.totalHeight); // 更新容器宽度 var containerWidth = columns * itemWidth + (columns - 1) * gap; if (_this.state.containerWidth !== containerWidth) { _this.setState({ containerWidth: containerWidth }); } }; // =================== 可视区域计算方法 =================== /** * 计算当前可视区域内的项目 * @private * @returns {boolean} 可见项目是否发生变化 * @description 基于当前滚动位置计算需要渲染的项目列表 */ _this.calculateVisibleItems = function () { var _a = _this.props, columns = _a.columns, containerHeight = _a.containerHeight, _b = _a.overscan, overscan = _b === void 0 ? VirtualWaterfall.defaultProps.overscan : _b; var data = _this.renderData; var scrollTop = _this.currentScrollTop; // 计算可视区域范围(包含预加载区域) var viewportTop = scrollTop - overscan; var viewportBottom = scrollTop + containerHeight + overscan; var visibleIndices = []; // 优化搜索范围:基于上次可见范围缩小搜索区域 var _c = _this.getOptimizedSearchRange(columns), searchStartIndex = _c.searchStartIndex, searchEndIndex = _c.searchEndIndex; // console.log('calculateVisibleItems', searchStartIndex, searchEndIndex); // 执行可见性检测 var _d = _this.findVisibleItems(visibleIndices, viewportTop, viewportBottom, searchStartIndex, searchEndIndex), newVisibleStartIndex = _d.newVisibleStartIndex, newVisibleEndIndex = _d.newVisibleEndIndex; // console.log('findVisibleItems', newVisibleStartIndex, newVisibleEndIndex); // 更新可见范围索引 _this.updateVisibleIndices(newVisibleStartIndex, newVisibleEndIndex); // 处理可见项目的变化 return _this.handleVisibleItemsChange(visibleIndices, data); }; // 初始化数据 _this.renderData = props.data; // 初始化下拉刷新相关 if (props.hasPullDownRefresh !== undefined) { _this.hasPullDownRefresh = props.hasPullDownRefresh; } _this.pullDownRefreshThreshold = props.pullDownRefreshThreshold !== undefined ? props.pullDownRefreshThreshold : 50; // 初始化列高度数组 _this.columnHeights = new Array(props.columns).fill(0); _this.state = { containerWidth: 0 }; return _this; } /** =================== 生命周期方法 =================== */ /** * 组件挂载后的回调 * @description 计算布局和渲染可视区域 */ VirtualWaterfall.prototype.componentDidMount = function () { // 初始化:计算所有项目布局 this.layoutItems(0, this.renderData.length); // 初始化:计算可视区域并渲染 var hasVisibleChange = this.calculateVisibleItems(); if (hasVisibleChange) { this.forceUpdate(); } }; /** * 组件卸载前的回调 * @description 清理缓存 */ VirtualWaterfall.prototype.componentWillUnmount = function () { // 重置缓存 this.resetCache(); }; /** * 组件更新前的回调 * @param {VirtualWaterfallProps} nextProps - 新的属性 * @param {VirtualWaterfallState} nextState - 新的状态 * @param {any} nextContext - 新的上下文 * @description 检查数据变化并相应地更新布局 */ VirtualWaterfall.prototype.componentWillUpdate = function (nextProps, nextState, nextContext) { var _this = this; // 检查数据是否发生变化 if (nextProps.data !== this.props.data) { // console.log('componentWillUpdate', nextProps.data); // 手动置空数据 this.renderData = []; this.reset(); // 延迟一帧,避免 Preact 对 无 key 变化时,对相似对象重用的异常处理 setTimeout(function () { // 更新数据 _this.renderData = nextProps.data; // 更新后,重新计算布局和可视区域 _this.reset(); }, 0); } }; /** * 组件更新后的回调 * @param {VirtualWaterfallProps} prevProps - 旧的属性 * @param {VirtualWaterfallState} prevState - 旧的状态 * @param {any} snapshot - 快照 * @description 检查属性变化并相应地更新布局 */ VirtualWaterfall.prototype.componentDidUpdate = function (prevProps, prevState, snapshot) { if (prevProps.itemWidth !== this.props.itemWidth) { this.reset(); } else if (prevProps.columns !== this.props.columns) { this.reset(); } else if (prevProps.gap !== this.props.gap) { this.reset(); } else if (prevProps.overscan !== this.props.overscan) { this.reset(); } }; /** =================== 外部可调用方法 =================== */ /** * 重新初始化 * @description 重新初始化瀑布流组件,包括清理缓存、重新计算布局、重新计算可视区域、重新渲染组件 */ VirtualWaterfall.prototype.reset = function () { this.resetCache(); this.layoutItems(0, this.renderData.length); this.calculateVisibleItems(); this.forceUpdate(); }; /** * 获取当前滚动位置 * @returns {number} 当前滚动位置 */ VirtualWaterfall.prototype.getScrollTop = function () { return this.currentScrollTop; }; /** * 滚动到指定位置 * @param {number} scrollLeft - 滚动到距离左侧的位置 * @param {number} scrollTop - 滚动到距离上方的位置 */ VirtualWaterfall.prototype.scrollTo = function (scrollLeft, scrollTop) { var _a, _b, _c, _d; if (((_a = this.containerRef.current) === null || _a === void 0 ? void 0 : _a.scrollLeft) !== undefined && ((_b = this.containerRef.current) === null || _b === void 0 ? void 0 : _b.scrollLeft) !== scrollLeft) { this.containerRef.current.scrollLeft = scrollLeft; } if (((_c = this.containerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== undefined && ((_d = this.containerRef.current) === null || _d === void 0 ? void 0 : _d.scrollTop) !== scrollTop) { this.containerRef.current.scrollTop = scrollTop; } }; /** * 获取指定索引的项目 * @param {number} index - 要获取的项目索引 * @returns {any} 项目数据 */ VirtualWaterfall.prototype.getLayoutInfoByIndex = function (index) { var layout = this.cachedLayouts[index]; return { x: layout.x, y: layout.y, width: layout.width, height: layout.height, columnIndex: layout.columnIndex, index: layout.index, }; }; /** * 获取可视区域项目索引的拷贝 * @returns {number[]} */ VirtualWaterfall.prototype.getVisibleIndices = function () { return __spreadArray([], this.lastVisibleIndices, true); }; /** * 获取总内容高度 * @returns {number} */ VirtualWaterfall.prototype.getContentHeight = function () { return this.totalHeight; }; /** * 更新指定索引的项目 * @param {number} index - 要更新的项目索引 * @param {any} item - 新的项目数据 */ VirtualWaterfall.prototype.updateItemByIndex = function (index, item) { this.renderData[index] = item; // 获取需要刷新的索引 var refreshIndices = [index]; // 回收删除的节点 this.handleExitingItems(refreshIndices); // 重新设置可视区域元素 this.handleEnteringItems(refreshIndices, this.renderData); // 更新数据后,强制更新 this.forceUpdate(); }; /** * 删除指定索引的项目 * @param {number} index - 要删除的项目索引 */ VirtualWaterfall.prototype.removeItemByIndex = function (index) { // console.log('removeItemByIndex', index, this.props.data); // 获取需要刷新的索引 var refreshIndices = this.lastVisibleIndices.filter(function (i) { return i >= index; }); // 回收删除的节点 this.handleExitingItems(refreshIndices); // 从数据中删除 this.renderData.splice(index, 1); this.cachedLayouts.pop(); // 需要重新计算布局的信息 var needLayoutIndex = index; var needLayoutCount = this.renderData.length - needLayoutIndex + 1; // console.log('removeItemByIndex', needLayoutIndex, needLayoutCount); // 重新计算布局 this.layoutItems(needLayoutIndex, needLayoutCount); // 如果 删除的索引 在最后一页,则需要减少 refreshIndices 重新渲染的数量,以及 lastVisibleIndices 的显示数量 if (index + refreshIndices.length > this.renderData.length) { refreshIndices.pop(); this.lastVisibleIndices.pop(); } // 重新设置可视区域元素 this.handleEnteringItems(refreshIndices, this.renderData); // 删除数据后,强制更新 this.forceUpdate(); }; /** * 添加子数据 * @param {any[]} data - 要添加的数据 */ VirtualWaterfall.prototype.addSubData = function (data) { // 获取旧数据长度 var oldLength = this.renderData.length; // console.log('oldData', this.renderData.slice()); // console.log('addData', data.slice()); // 添加数据 this.renderData = __spreadArray(__spreadArray([], this.renderData, true), data, true); // console.log('addSubData', this.renderData); // 重新计算布局,从 oldLength 开始(新数据开始索引),计算 data.length 个项目 this.layoutItems(oldLength, data.length); var hasVisibleChange = this.calculateVisibleItems(); if (hasVisibleChange) { this.forceUpdate(); } return this.renderData; }; /** * 在指定索引添加子数据 * @param {any[]} data - 要添加的数据 * @param {number} index - 要添加的数据索引 * * */ VirtualWaterfall.prototype.addSubDataAtIndex = function (data, index) { var _a, _b; // 在指定索引处插入数据 (_a = this.renderData).splice.apply(_a, __spreadArray([index, 0], data, false)); // 更新缓存布局:在cachedLayouts的index位置插入data.length个空位 var emptyLayouts = new Array(data.length).fill(null); (_b = this.cachedLayouts).splice.apply(_b, __spreadArray([index, 0], emptyLayouts, false)); // 需要重新计算布局的信息:从插入位置开始到结束 var needLayoutIndex = index; var needLayoutCount = this.renderData.length - needLayoutIndex + 1; // 重新计算布局 this.layoutItems(needLayoutIndex, needLayoutCount); // 获取需要刷新的索引(当前可见区域中索引大于等于index的项目) var refreshIndices = this.lastVisibleIndices.filter(function (i) { return i >= index; }); // 回收受影响的项目 this.handleExitingItems(refreshIndices); // 重新设置可视区域元素 this.handleEnteringItems(refreshIndices, this.renderData); // 强制更新 this.forceUpdate(); }; /** * 从指定索引开始重新计算布局 * @param {number} index - 要重新计算布局的索引 */ VirtualWaterfall.prototype.layoutItemsFromIndex = function (index) { // 获取需要刷新的索引 var refreshIndices = this.lastVisibleIndices.filter(function (i) { return i >= index; }); // 回收删除的节点 this.handleExitingItems(refreshIndices); // 需要重新计算布局的信息 var needLayoutIndex = index; var needLayoutCount = this.renderData.length - needLayoutIndex; // 重新计算布局 this.layoutItems(needLayoutIndex, needLayoutCount); // 重新设置可视区域元素 this.handleEnteringItems(refreshIndices, this.renderData); // 删除数据后,强制更新 this.forceUpdate(); }; /** * 检查下拉刷新状态 * @param {number} scrollTop - 当前滚动位置 * @description 检查下拉刷新状态,并触发下拉刷新 */ VirtualWaterfall.prototype.checkAndTriggerPullDownRefresh = function (scrollTop) { var _a, _b, _c, _d; // 下拉刷新状态 if (this.hasPullDownRefresh()) { // 只有在 PixUI 中,滚动位置才会小于 0 if (scrollTop < 0) { // 如果下拉刷新状态不是 ready,则设置为 pulling if (this.pullDownRefreshState !== 'ready') { this.pullDownRefreshState = 'pulling'; } // 记录下拉刷新距离 this.pullDownRefreshDistance = scrollTop; (_b = (_a = this.props).onPullDown) === null || _b === void 0 ? void 0 : _b.call(_a, scrollTop); // 如果下拉刷新状态不是 ready,则设置为 ready if (this.pullDownRefreshState !== 'ready' && scrollTop < -this.pullDownRefreshThreshold) { this.pullDownRefreshState = 'ready'; // 触发下拉刷新 this.triggerPullDownRefresh(); this.forceUpdate(); } } else { if (this.pullDownRefreshState !== 'idle') { // 还原为 idle 状态 this.pullDownRefreshState = 'idle'; this.pullDownRefreshDistance = 0; (_d = (_c = this.props).onPullDownEnd) === null || _d === void 0 ? void 0 : _d.call(_c); this.forceUpdate(); } } } }; /** * 检查并触发加载更多数据 * @private * @param {number} scrollTop - 当前滚动位置 * @description 当滚动接近底部时触发加载更多回调 */ VirtualWaterfall.prototype.checkAndTriggerLoadMore = function (scrollTop) { if (this.props.hasMore && this.props.hasMore() && this.props.loadMore) { var containerHeight = this.props.containerHeight; var totalContentHeight = this.totalHeight; // 提前触发加载:当滚动距离底部还有指定阈值时就开始加载 var loadMoreThreshold = this.props.loadMoreThreshold || VirtualWaterfall.defaultProps.loadMoreThreshold; var distanceFromBottom = totalContentHeight - (scrollTop + containerHeight); // 如果滚动距离底部还有指定阈值,则触发加载更多 if (distanceFromBottom <= loadMoreThreshold) { if (!this.isLoadingMore) { this.isLoadingMore = true; this.props.loadMore(); this.forceUpdate(); } } else { this.isLoadingMore = false; } } }; /** * 获取优化的搜索范围 * @private * @param {number} columns - 列数 * @returns {Object} 搜索范围对象 * @returns {number} returns.searchStartIndex - 搜索开始索引 * @returns {number} returns.searchEndIndex - 搜索结束索引 * @description 基于上次可见范围优化搜索区域,提高性能 */ VirtualWaterfall.prototype.getOptimizedSearchRange = function (columns) { var searchStartIndex = 0; var searchEndIndex = this.layoutedItemCount; // 如果有上次的可见范围记录,则优化搜索范围 if (this.lastVisibleIndices.length > 0) { var lastFirstVisible = this.lastVisibleIndices[0]; var lastLastVisible = this.lastVisibleIndices[this.lastVisibleIndices.length - 1]; // 扩展搜索范围以防遗漏(瀑布流中相邻项目可能距离较远) var bufferSize = Math.max(columns * 2, 10); searchStartIndex = Math.max(0, lastFirstVisible - bufferSize); searchEndIndex = Math.min(this.layoutedItemCount, lastLastVisible + bufferSize); } return { searchStartIndex: searchStartIndex, searchEndIndex: searchEndIndex }; }; /** * 在指定范围内查找可见项目 * @private * @param {number[]} visibleIndices - 可见项目索引数组 * @param {number} viewportTop - 视口顶部位置 * @param {number} viewportBottom - 视口底部位置 * @param {number} searchStartIndex - 搜索开始索引 * @param {number} searchEndIndex - 搜索结束索引 * @returns {Object} 可见范围对象 * @returns {number} returns.newVisibleStartIndex - 新的可见开始索引 * @returns {number} returns.newVisibleEndIndex - 新的可见结束索引 */ VirtualWaterfall.prototype.findVisibleItems = function (visibleIndices, viewportTop, viewportBottom, searchStartIndex, searchEndIndex) { var newVisibleStartIndex = -1; var newVisibleEndIndex = -1; var foundFirstVisible = false; // 第一遍:在优化范围内搜索 for (var i = searchStartIndex; i < searchEndIndex; i++) { var layout = this.cachedLayouts[i]; var itemTop = layout.y; var itemBottom = layout.y + layout.height; if (itemBottom >= viewportTop && itemTop <= viewportBottom) { if (!foundFirstVisible) { newVisibleStartIndex = i; foundFirstVisible = true; } visibleIndices.push(i); newVisibleEndIndex = i; } else if (foundFirstVisible && itemTop > viewportBottom) { // 已经超出可视区域,可以提前结束 break; } } // 如果优化范围内没找到足够项目,则全范围搜索 if (!foundFirstVisible || visibleIndices.length === 0) { return this.fullRangeSearch(visibleIndices, viewportTop, viewportBottom); } return { newVisibleStartIndex: newVisibleStartIndex, newVisibleEndIndex: newVisibleEndIndex }; }; /** * 全范围搜索(备用方案) * @private * @param {number[]} visibleIndices - 可见项目索引数组 * @param {number} viewportTop - 视口顶部位置 * @param {number} viewportBottom - 视口底部位置 * @returns {Object} 可见范围对象 * @returns {number} returns.newVisibleStartIndex - 新的可见开始索引 * @returns {number} returns.newVisibleEndIndex - 新的可见结束索引 * @description 当优化搜索失败时的全范围搜索备用方案 */ VirtualWaterfall.prototype.fullRangeSearch = function (visibleIndices, viewportTop, viewportBottom) { visibleIndices.length = 0; var newVisibleStartIndex = -1; var newVisibleEndIndex = -1; var foundFirstVisible = false; for (var i = 0; i < this.layoutedItemCount; i++) { var layout = this.cachedLayouts[i]; var itemTop = layout.y; var itemBottom = layout.y + layout.height; if (itemBottom >= viewportTop && itemTop <= viewportBottom) { if (!foundFirstVisible) { newVisibleStartIndex = i; foundFirstVisible = true; } visibleIndices.push(i); newVisibleEndIndex = i; } else if (foundFirstVisible && itemTop > viewportBottom) { break; } } return { newVisibleStartIndex: newVisibleStartIndex, newVisibleEndIndex: newVisibleEndIndex }; }; /** * 更新可见范围索引 * @private * @param {number} newVisibleStartIndex - 新的可见开始索引 * @param {number} newVisibleEndIndex - 新的可见结束索引 * @description 更新内部维护的可见范围索引值 */ VirtualWaterfall.prototype.updateVisibleIndices = function (newVisibleStartIndex, newVisibleEndIndex) { if (newVisibleStartIndex !== -1) { this.visibleStartIndex = newVisibleStartIndex; } if (newVisibleEndIndex !== -1) { this.visibleEndIndex = newVisibleEndIndex; } }; /** * 处理可见项目变化 * @private * @param {number[]} visibleIndices - 当前可见项目索引列表 * @param {any[]} data - 数据列表 * @returns {boolean} 是否有变化 * @description 比较新旧可见项目列表,处理进入和离开的项目 */ VirtualWaterfall.prototype.handleVisibleItemsChange = function (visibleIndices, data) { var _this = this; // 找出离开和进入可视区域的项目 var exitingIndices = this.lastVisibleIndices.filter(function (index) { return !visibleIndices.includes(index); }); var enteringIndices = visibleIndices.filter(function (index) { return !_this.lastVisibleIndices.includes(index); }); // console.log('handleVisibleItemsChange', exitingIndices, enteringIndices); // 检查是否有变化 var hasChange = exitingIndices.length > 0 || enteringIndices.length > 0; if (!hasChange) { return false; } // 处理离开可视区域的项目 this.handleExitingItems(exitingIndices); // 处理进入可视区域的项目 this.handleEnteringItems(enteringIndices, data); // 记录当前可见项目 this.lastVisibleIndices = visibleIndices; return true; }; /** * 处理离开可视区域的项目 * @private * @param {number[]} exitingIndices - 离开的项目索引列表 * @description 根据缓存策略处理离开可视区域的项目 */ VirtualWaterfall.prototype.handleExitingItems = function (exitingIndices) { var _a, _b, _c, _d; var _loop_1 = function (index) { var waterfallItem = this_1.waterfallItemsMap.get(index); // console.log('handleExitingItems', index, waterfallItem); if (!waterfallItem) return "continue"; // 元素隐藏 if (waterfallItem.refs) { (_b = (_a = this_1.props).onItemHide) === null || _b === void 0 ? void 0 : _b.call(_a, waterfallItem.data, index, waterfallItem.refs); } else { (_d = (_c = this_1.props).onItemHide) === null || _d === void 0 ? void 0 : _d.call(_c, waterfallItem.data, index); } if (this_1.props.useCache) { // 启用缓存:将元素移到回收池 this_1.recycleElement(waterfallItem); } else { // 不启用缓存:直接从可见列表移除 var itemIndex = this_1.waterfallItems.findIndex(function (item) { return item.key === waterfallItem.key; }); if (itemIndex !== -1) { this_1.waterfallItems.splice(itemIndex, 1); this_1.waterfallItemsMap.delete(index); } } }; var this_1 = this; // console.log('handleExitingItems', exitingIndices); for (var _i = 0, exitingIndices_1 = exitingIndices; _i < exitingIndices_1.length; _i++) { var index = exitingIndices_1[_i]; _loop_1(index); } }; /** * 处理进入可视区域的项目 * @private * @param {number[]} enteringIndices - 进入的项目索引列表 * @param {any[]} data - 数据列表 * @description 为进入可视区域的项目创建或复用渲染元素 */ VirtualWaterfall.prototype.handleEnteringItems = function (enteringIndices, data) { var _a, _b, _c, _d; // console.log('handleEnteringItems', enteringIndices, data); for (var _i = 0, enteringIndices_1 = enteringIndices; _i < enteringIndices_1.length; _i++) { var index = enteringIndices_1[_i]; var layout = this.cachedLayouts[index]; var item = data[layout.index]; var itemType = this.getItemType(item); // console.log('handleEnteringItems', index, layout, item, itemType); // 尝试从缓存池获取可复用的元素 var cachedElements = this.recycledWaterfallItemsByType.get(itemType); var enterItem = void 0; if (this.props.useCache && cachedElements && cachedElements.length > 0) { // 从缓存复用元素 enterItem = this.reuseElementFromCache(cachedElements, item, layout, itemType, index); } else { // 创建新元素 enterItem = this.createNewElement(item, layout, itemType, index); } // 元素曝光 if (enterItem.refs) { (_b = (_a = this.props).onItemExpose) === null || _b === void 0 ? void 0 : _b.call(_a, item, index, enterItem.refs); } else { (_d = (_c = this.props).onItemExpose) === null || _d === void 0 ? void 0 : _d.call(_c, item, index); } } }; /** * 从缓存复用元素 * @private * @param {WaterfallItem[]} cachedElements - 缓存的元素列表 * @param {any} item - 数据项 * @param {CachedItemLayout} layout - 布局信息 * @param {string} itemType - 项目类型 * @param {number} index - 项目索引 * @description 从缓存池中获取元素并更新其内容和位置 */ VirtualWaterfall.prototype.reuseElementFromCache = function (cachedElements, item, layout, itemType, index) { var cached = cachedElements.pop(); // 更新缓存元素的数据和位置 cached.data = item; cached.x = layout.x; cached.y = layout.y; cached.width = layout.width; cached.height = layout.height; cached.columnIndex = layout.columnIndex; cached.index = layout.index; cached.itemType = itemType; // console.log('reuseElementFromCache', index, cached); // 更新组件的位置信息 var cachedVNodeRef = cached.vNode.ref; if (cachedVNodeRef.current) { cachedVNodeRef.current.updateState({ x: layout.x, y: layout.y, width: layout.width, height: layout.height, }); } // 更新元素内容 if (cached.refs && this.props.updateItem) { // 有refs的情况:通过updateItem更新内容 this.props.updateItem(item, index, cached.refs); } else if (cachedVNodeRef.current) { // 没有refs的情况:重新渲染元素 var element = this.props.renderItem(item, index).element; cachedVNodeRef.current.updateElement(element); } this.waterfallItemsMap.set(index, cached); return cached; }; /** * 创建新元素 * @private * @param {any} item - 数据项 * @param {CachedItemLayout} layout - 布局信息 * @param {string} itemType - 项目类型 * @param {number} index - 项目索引 * @description 为新进入可视区域的项目创建渲染元素 */ VirtualWaterfall.prototype.createNewElement = function (item, layout, itemType, index) { var _a = this.props.renderItem(item, index), element = _a.element, refs = _a.refs; var key = "waterfall-".concat(this.keyIndex++); // console.log('createNewElement', item, layout, itemType, index); // 创建瀑布流项目组件 var vNodeRef = createRef(); // 注意!这里在 重用VNode 的情况下去掉key。避免踩到 preact 重用 vNode,子节点的父节点引用没有清理,导致引用图片的内存泄漏问题。 // 然后在 非重用VNode 的情况下下,需要开启key,避免 VNode的渲染异常。 var vNode = (h(WaterfallItemComponent, { ref: vNodeRef, key: this.props.useCache ? undefined : key, element: element, x: layout.x, y: layout.y, width: layout.width, height: layout.height })); // 创建瀑布流项目数据 var waterfallItem = { // 数据相关 data: item, itemType: itemType, index: layout.index, // 布局相关 x: layout.x, y: layout.y, width: layout.width, height: layout.height, columnIndex: layout.columnIndex, // 渲染相关 key: key, element: element, vNode: vNode, vNodeRef: vNodeRef, refs: refs, }; // 添加到可见项目列表 this.waterfallItems.push(waterfallItem); this.waterfallItemsMap.set(index, waterfallItem); return waterfallItem; }; /** =================== 下拉刷新事件处理方法 =================== */ /** * 触发下拉刷新 * @private */ VirtualWaterfall.prototype.triggerPullDownRefresh = function () { // 调用用户提供的刷新回调 if (this.props.onPullDownRefresh) { this.props.onPullDownRefresh(); } }; ; /** * 重置下拉刷新状态 * @private */ VirtualWaterfall.prototype.resetPullDownRefresh = function () { this.pullDownRefreshState = 'idle'; this.pullDownRefreshDistance = 0; }; ; /** * 完成下拉刷新(外部调用) * @public * @description 手动完成下拉刷新 */ VirtualWaterfall.prototype.finishPullDownRefresh = function () { this.resetPullDownRefresh(); }; ; /** * 处理滚动相关配置 * @private * @returns {VirtualListScrollProps} 滚动相关配置 */ VirtualWaterfall.prototype.handleScrollProps = function () { var scrollProps = { movementType: 'elastic', scrollSensitivity: 1, inertiaVersion: 2, decelerationRate: 0.135, staticVelocityDrag: 100, frictionCoefficient: 2.0, }; if (this.props.scrollProps) { /* 兼容双问号 */ if (this.props.scrollProps.movementType !== null && this.props.scrollProps.movementType !== undefined) { scrollProps.movementType = this.props.scrollProps.movementType; } if (this.props.scrollProps.scrollSensitivity !== null && this.props.scrollProps.scrollSensitivity !== undefined) { scrollProps.scrollSensitivity = this.props.scrollProps.scrollSensitivity; } if (this.props.scrollProps.inertiaVersion !== null && this.props.scrollProps.inertiaVersion !== undefined) { scrollProps.inertiaVersion = this.props.scrollProps.inertiaVersion; } if (this.props.scrollProps.decelerationRate !== null && this.props.scrollProps.decelerationRate !== undefined) { scrollProps.decelerationRate = this.props.scrollProps.decelerationRate; } if (this.props.scrollProps.staticVelocityDrag !== null && this.props.scrollProps.staticVelocityDrag !== undefined) { scrollProps.staticVelocityDrag = this.props.scrollProps.staticVelocityDrag; } if (this.props.scrollProps.frictionCoefficient !== null && this.props.scrollProps.frictionCoefficient !== undefined) { scrollProps.frictionCoefficient = this.props.scrollProps.frictionCoefficient; } } return scrollProps; }; /** =================== 渲染方法 =================== */ /** * 渲染组件 * @returns {JSX.Element} 渲染的JSX元素 * @description 渲染虚拟瀑布流列表的主要结构 */ VirtualWaterfall.prototype.render = function () { var _a; var containerHeight = this.props.containerHeight; var containerWidth = this.state.containerWidth; // console.log('render', this.waterfallItems.length); // 计算加载更多高度,主要用于兼容 Unity 和 UE 的滚动高度运算时候,的额外区域的运算错误。 var loadMoreHeight = 0; if (this.loadMoreRef) { var loadMoreRect = (_a = this.loadMoreRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); if (loadMoreRect && loadMoreRect.height) { loadMoreHeight = loadMoreRect.height; } } // 计算总内容高度 var totalContentHeight = this.totalHeight + loadMoreHeight; var scrollProps = this.handleScrollProps(); return (h("div", { ref: this.containerRef, id: this.props.rootId || 'VirtualWaterfallRoot', className: this.props.rootClassName || 'VirtualWaterfallRoot', style: __assign({ height: "".concat(containerHeight, "px"), overflow: 'scroll', position: 'relative', flexDirection: 'column' }, this.props.rootStyle), "movement-type": scrollProps.movementType, "scroll-sensitivity": scrollProps.scrollSensitivity, "inertia-version": scrollProps.inertiaVersion, "deceleration-rate": scrollProps.decelerationRate, "static-velocity-drag": scrollProps.staticVelocityDrag, "friction-coefficient": scrollProps.frictionCoefficient, onScroll: this.handleScroll }, h("div", { ref: this.contentRef, id: 'VirtualWaterfallContent', className: 'VirtualWaterfallContent', style: __assign({ display: 'flex', flexShrink: 0, position: 'relative', width: "".concat(containerWidth, "px"), height: "".concat(totalContentHeight, "px") }, this.props.contentStyle) }, this.props.renderPullDownArea && (h("div", { style: { position: 'absolute', bottom: '100%', left: '0', width: '100%', } }, this.props.renderPullDownArea())), this.waterfallItems.map(function (item) { return item.vNode; }), this.props.renderLoadMoreArea && this.waterfallItems.length > 0 && (h("div", { id: "loadMoreWrap", ref: this.loadMoreRef, style: { position: 'absolute', left: 0, top: "".concat(this.totalHeight, "px"), width: "".concat(containerWidth, "px") } }, this.props.renderLoadMoreArea()))))); }; VirtualWaterfall.defaultProps = { loadMoreThreshold: 100, pullDownRefreshThreshold: 50, gap: 10, overscan: 0, }; return VirtualWaterfall; }(Component)); export { VirtualWaterfall };