Files
location_point/components/z-paging/js/modules/virtual-list.js
2026-02-23 16:31:39 +08:00

375 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// [z-paging]虚拟列表模块
import u from '.././z-paging-utils'
import c from '.././z-paging-constant'
import Enum from '.././z-paging-enum'
const ZPVirtualList = {
props: {
//是否使用虚拟列表,默认为否
useVirtualList: {
type: Boolean,
default: u.gc('useVirtualList', false)
},
//是否在z-paging内部循环渲染列表(内置列表)默认为否。若use-virtual-list为true则此项恒为true
useInnerList: {
type: Boolean,
default: u.gc('useInnerList', false)
},
//强制关闭inner-list默认为false如果为true将强制关闭innerList适用于开启了虚拟列表后需要强制关闭inner-list的情况
forceCloseInnerList: {
type: Boolean,
default: u.gc('forceCloseInnerList', false)
},
//内置列表cell的key名称仅nvue有效在nvue中开启use-inner-list时必须填此项
cellKeyName: {
type: String,
default: u.gc('cellKeyName', '')
},
//innerList样式
innerListStyle: {
type: Object,
default: function() {
return u.gc('innerListStyle', {});
}
},
//innerCell样式
innerCellStyle: {
type: Object,
default: function() {
return u.gc('innerCellStyle', {});
}
},
//预加载的列表可视范围(列表高度)页数默认为7即预加载当前页及上下各7页的cell。此数值越大则虚拟列表中加载的dom越多内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
preloadPage: {
type: [Number, String],
default: u.gc('preloadPage', 7),
validator: (value) => {
if (value <= 0) u.consoleErr('preload-page必须大于0');
return value > 0;
}
},
//虚拟列表cell高度模式默认为fixed也就是每个cell高度完全相同将以第一个cell高度为准进行计算。可选值【dynamic】即代表高度是动态非固定的【dynamic】性能低于【fixed】。
cellHeightMode: {
type: String,
default: u.gc('cellHeightMode', 'fixed')
},
//虚拟列表列数默认为1。常用于每行有多列的情况例如每行有2列数据需要将此值设置为2
virtualListCol: {
type: [Number, String],
default: u.gc('virtualListCol', 1)
},
//虚拟列表scroll取样帧率默认为60过高可能出现卡顿等问题
virtualScrollFps: {
type: [Number, String],
default: u.gc('virtualScrollFps', 60)
},
},
data() {
return {
virtualListKey: u.getInstanceId(),
virtualPageHeight: 0,
virtualCellHeight: 0,
virtualScrollTimeStamp: 0,
virtualList: [],
virtualPlaceholderTopHeight: 0,
virtualPlaceholderBottomHeight: 0,
virtualTopRangeIndex: 0,
virtualBottomRangeIndex: 0,
lastVirtualTopRangeIndex: 0,
lastVirtualBottomRangeIndex: 0,
virtualHeightCacheList: [],
getCellHeightRetryCount: {
fixed: 0,
dynamic: 0
},
pagingOrgTop: -1,
updateVirtualListFromDataChange: false
}
},
watch: {
realTotalData(newVal) {
// #ifndef APP-NVUE
if (this.finalUseVirtualList) {
this.updateVirtualListFromDataChange = true;
this.$nextTick(() => {
if (!newVal.length) {
this._resetDynamicListState(!this.isUserPullDown);
}
this.getCellHeightRetryCount.fixed = 0;
this.finalUseVirtualList && newVal.length && this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight();
this.finalUseVirtualList && this._updateVirtualScroll(this.oldScrollTop);
})
}
// #endif
},
virtualList(newVal){
this.$emit('update:virtualList', newVal);
this.$emit('virtualListChange', newVal);
}
},
computed: {
finalUseVirtualList() {
if (this.useVirtualList && this.usePageScroll){
u.consoleErr('使用页面滚动时,开启虚拟列表无效!');
}
return this.useVirtualList && !this.usePageScroll;
},
finalUseInnerList() {
return this.useInnerList || (this.finalUseVirtualList && !this.forceCloseInnerList)
},
finalCellKeyName() {
// #ifdef APP-NVUE
if (this.finalUseVirtualList){
if (!this.cellKeyName.length){
u.consoleErr('在nvue中开启use-virtual-list必须设置cell-key-name否则将可能导致列表渲染错误');
}
}
// #endif
return this.cellKeyName;
},
finalVirtualPageHeight(){
return this.virtualPageHeight > 0 ? this.virtualPageHeight : this.windowHeight;
return virtualPageHeight * this.preloadPage;
},
virtualRangePageHeight(){
return this.finalVirtualPageHeight * this.preloadPage;
},
virtualScrollDisTimeStamp() {
return 1000 / this.virtualScrollFps;
},
},
methods: {
//初始化虚拟列表
_virtualListInit() {
this.$nextTick(() => {
setTimeout(() => {
this._getNodeClientRect('.zp-scroll-view').then(node => {
if (node && node.length) {
this.pagingOrgTop = node[0].top;
this.virtualPageHeight = node[0].height;
}
});
}, 100);
})
},
//cellHeightMode为fixed时获取第一个cell高度
_updateFixedCellHeight() {
this.$nextTick(() => {
const updateFixedCellHeightTimeout = setTimeout(() => {
this._getNodeClientRect(`#zp-${0}`,this.finalUseInnerList).then(cellNode => {
const hasCellNode = cellNode && cellNode.length;
if (!hasCellNode) {
clearTimeout(updateFixedCellHeightTimeout);
if (this.getCellHeightRetryCount.fixed > 10) {
u.consoleErr('获取虚拟列表cell高度失败可能是for循环cell处没有写:id="`zp-${item.zp_index}`",请检查您的代码!')
return;
}
this.getCellHeightRetryCount.fixed++;
this._updateFixedCellHeight();
} else {
this.virtualCellHeight = cellNode[0].height;
this._updateVirtualScroll(this.oldScrollTop);
}
});
}, 100);
})
},
//cellHeightMode为dynamic时获取每个cell高度
_updateDynamicCellHeight(list) {
this.$nextTick(() => {
const updateDynamicCellHeightTimeout = setTimeout(async () => {
for (let i = 0; i < list.length; i++) {
let item = list[i];
const cellNode = await this._getNodeClientRect(`#zp-${item[c.listCellIndexKey]}`,this.finalUseInnerList);
const hasCellNode = cellNode && cellNode.length;
const currentHeight = hasCellNode ? cellNode[0].height : 0;
if (!hasCellNode) {
clearTimeout(updateDynamicCellHeightTimeout);
this.virtualHeightCacheList = this.virtualHeightCacheList.slice(-i);
if (this.getCellHeightRetryCount.dynamic > 10) {
u.consoleErr('获取虚拟列表cell高度失败可能是for循环cell处没有写:id="`zp-${item.zp_index}`",请检查您的代码!')
return;
}
this.getCellHeightRetryCount.dynamic++;
this._updateDynamicCellHeight(list);
break;
}
let lastHeightCache = null;
if (this.virtualHeightCacheList.length) {
lastHeightCache = this.virtualHeightCacheList.slice(-1)[0];
}
const lastHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
this.virtualHeightCacheList.push({
height: currentHeight,
lastHeight: lastHeight,
totalHeight: lastHeight + currentHeight
});
}
this._updateVirtualScroll(this.oldScrollTop);
}, 100)
})
},
//设置cellItem的index
_setCellIndex(list, isFirstPage) {
let lastItem = null;
let lastItemIndex = 0;
if (!isFirstPage) {
lastItemIndex = this.realTotalData.length;
if (this.realTotalData.length) {
lastItem = this.realTotalData.slice(-1)[0];
}
if (lastItem && lastItem[c.listCellIndexKey] !== undefined) {
lastItemIndex = lastItem[c.listCellIndexKey] + 1;
}
} else {
this._resetDynamicListState();
}
for (let i = 0; i < list.length; i++) {
let item = list[i];
if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
item = {item};
}
item[c.listCellIndexKey] = lastItemIndex + i;
item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[c.listCellIndexKey]}`;
list[i] = item;
}
this.getCellHeightRetryCount.dynamic = 0;
this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list);
},
//更新scroll滚动
_updateVirtualScroll(scrollTop, scrollDiff = 0) {
const currentTimeStamp = u.getTime();
if (scrollTop === 0) {
this._resetTopRange();
}
if (scrollTop !== 0 && this.virtualScrollTimeStamp && currentTimeStamp - this.virtualScrollTimeStamp <= this.virtualScrollDisTimeStamp) {
return;
}
this.virtualScrollTimeStamp = Number(currentTimeStamp);
let scrollIndex = 0;
const cellHeightMode = this.cellHeightMode;
if (cellHeightMode === Enum.CellHeightMode.Fixed) {
scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
this._updateFixedTopRangeIndex(scrollIndex);
this._updateFixedBottomRangeIndex(scrollIndex);
} else if(cellHeightMode === Enum.CellHeightMode.Dynamic) {
const scrollDirection = scrollDiff > 0 ? 'top' : 'bottom';
const rangePageHeight = this.virtualRangePageHeight;
const topRangePageOffset = scrollTop - rangePageHeight;
const bottomRangePageOffset = scrollTop + this.finalVirtualPageHeight + rangePageHeight;
let virtualBottomRangeIndex = 0;
let virtualPlaceholderBottomHeight = 0;
let reachedLimitBottom = false;
let lastHeightCache = null;
const heightCacheList = this.virtualHeightCacheList;
if (heightCacheList.length) {
lastHeightCache = heightCacheList.slice(-1)[0];
}
let startTopRangeIndex = this.virtualTopRangeIndex;
if (scrollDirection === 'bottom') {
for (let i = startTopRangeIndex; i < heightCacheList.length;i++){
const heightCacheItem = heightCacheList[i];
if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
this.virtualTopRangeIndex = i;
this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
break;
}
}
} else {
let topRangeMatched = false;
for (let i = startTopRangeIndex; i >= 0;i--){
const heightCacheItem = heightCacheList[i];
if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
this.virtualTopRangeIndex = i;
this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
topRangeMatched = true;
break;
}
}
!topRangeMatched && this._resetTopRange();
}
for (let i = this.virtualTopRangeIndex; i < heightCacheList.length;i++){
const heightCacheItem = heightCacheList[i];
if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
virtualBottomRangeIndex = i;
virtualPlaceholderBottomHeight = lastHeightCache.totalHeight - heightCacheItem.totalHeight;
reachedLimitBottom = true;
break;
}
}
if (!reachedLimitBottom || this.virtualBottomRangeIndex === 0) {
this.virtualBottomRangeIndex = this.realTotalData.length ? this.realTotalData.length - 1 : this.pageSize;
this.virtualPlaceholderBottomHeight = 0;
} else {
this.virtualBottomRangeIndex = virtualBottomRangeIndex;
this.virtualPlaceholderBottomHeight = virtualPlaceholderBottomHeight;
}
this._updateVirtualList();
}
},
//更新fixedCell模式下topRangeIndex&placeholderTopHeight
_updateFixedTopRangeIndex(scrollIndex) {
let virtualTopRangeIndex = this.virtualCellHeight === 0 ? 0 : scrollIndex - parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) * this.preloadPage;
virtualTopRangeIndex *= this.virtualListCol;
virtualTopRangeIndex = Math.max(0, virtualTopRangeIndex);
this.virtualTopRangeIndex = virtualTopRangeIndex;
this.virtualPlaceholderTopHeight = (virtualTopRangeIndex / this.virtualListCol) * this.virtualCellHeight;
},
//更新fixedCell模式下bottomRangeIndex&placeholderBottomHeight
_updateFixedBottomRangeIndex(scrollIndex) {
let virtualBottomRangeIndex = this.virtualCellHeight === 0 ? this.pageSize : scrollIndex + parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) * (this.preloadPage + 1);
virtualBottomRangeIndex *= this.virtualListCol;
virtualBottomRangeIndex = Math.min(this.realTotalData.length, virtualBottomRangeIndex);
this.virtualBottomRangeIndex = virtualBottomRangeIndex;
this.virtualPlaceholderBottomHeight = (this.realTotalData.length - virtualBottomRangeIndex) * this.virtualCellHeight / this.virtualListCol;
this._updateVirtualList();
},
//更新virtualList
_updateVirtualList() {
const shouldUpdateList = this.updateVirtualListFromDataChange || (this.lastVirtualTopRangeIndex !== this.virtualTopRangeIndex || this.lastVirtualBottomRangeIndex !== this.virtualBottomRangeIndex);
if (shouldUpdateList) {
this.updateVirtualListFromDataChange = false;
this.lastVirtualTopRangeIndex = this.virtualTopRangeIndex;
this.lastVirtualBottomRangeIndex = this.virtualBottomRangeIndex;
this.virtualList = this.realTotalData.slice(this.virtualTopRangeIndex, this.virtualBottomRangeIndex + 1);
}
},
//重置动态cell模式下的高度缓存数据、虚拟列表和滚动状态
_resetDynamicListState(resetVirtualList = false) {
this.virtualHeightCacheList = [];
if (resetVirtualList) {
this.virtualList = [];
}
this.virtualTopRangeIndex = 0;
this.virtualPlaceholderTopHeight = 0;
},
//重置topRangeIndex和placeholderTopHeight
_resetTopRange() {
this.virtualTopRangeIndex = 0;
this.virtualPlaceholderTopHeight = 0;
this._updateVirtualList();
},
//检测虚拟列表当前滚动位置,如发现滚动位置不正确则重新计算虚拟列表相关参数(为解决在App中可能出现的长时间进入后台后打开App白屏的问题)
_checkVirtualListScroll() {
if (this.finalUseVirtualList) {
this.$nextTick(() => {
this._getNodeClientRect('.zp-paging-touch-view').then(node => {
const hasNode = node && node.length;
const currentTop = hasNode ? node[0].top : 0;
if (!hasNode || (currentTop === this.pagingOrgTop && this.virtualPlaceholderTopHeight !== 0)){
this._updateVirtualScroll(0);
}
});
})
}
}
}
}
export default ZPVirtualList;