// [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;