@pixui-dev/pixui-react-virtualwaterfall
Version:
pixui 高性能React虚拟瀑布流组件
1,151 lines (1,150 loc) • 50 kB
JavaScript
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;
// 获取最后一个项目的缓存列高度
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 };