virtual-list.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. // [z-paging]虚拟列表模块
  2. import u from '.././z-paging-utils'
  3. import c from '.././z-paging-constant'
  4. import Enum from '.././z-paging-enum'
  5. export default {
  6. props: {
  7. //是否使用虚拟列表,默认为否
  8. useVirtualList: {
  9. type: Boolean,
  10. default: u.gc('useVirtualList', false)
  11. },
  12. //在使用虚拟列表时,是否使用兼容模式,默认为否
  13. useCompatibilityMode: {
  14. type: Boolean,
  15. default: u.gc('useCompatibilityMode', false)
  16. },
  17. //使用兼容模式时传递的附加数据
  18. extraData: {
  19. type: Object,
  20. default: function() {
  21. return u.gc('extraData', {});
  22. }
  23. },
  24. //是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
  25. useInnerList: {
  26. type: Boolean,
  27. default: u.gc('useInnerList', false)
  28. },
  29. //强制关闭inner-list,默认为false,如果为true将强制关闭innerList,适用于开启了虚拟列表后需要强制关闭inner-list的情况
  30. forceCloseInnerList: {
  31. type: Boolean,
  32. default: u.gc('forceCloseInnerList', false)
  33. },
  34. //内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
  35. cellKeyName: {
  36. type: String,
  37. default: u.gc('cellKeyName', '')
  38. },
  39. //innerList样式
  40. innerListStyle: {
  41. type: Object,
  42. default: function() {
  43. return u.gc('innerListStyle', {});
  44. }
  45. },
  46. //innerCell样式
  47. innerCellStyle: {
  48. type: Object,
  49. default: function() {
  50. return u.gc('innerCellStyle', {});
  51. }
  52. },
  53. //预加载的列表可视范围(列表高度)页数,默认为7,即预加载当前页及上下各7页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
  54. preloadPage: {
  55. type: [Number, String],
  56. default: u.gc('preloadPage', 7),
  57. validator: (value) => {
  58. if (value <= 0) u.consoleErr('preload-page必须大于0!');
  59. return value > 0;
  60. }
  61. },
  62. //虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
  63. cellHeightMode: {
  64. type: String,
  65. default: u.gc('cellHeightMode', Enum.CellHeightMode.Fixed)
  66. },
  67. //虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
  68. virtualListCol: {
  69. type: [Number, String],
  70. default: u.gc('virtualListCol', 1)
  71. },
  72. //虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
  73. virtualScrollFps: {
  74. type: [Number, String],
  75. default: u.gc('virtualScrollFps', 80)
  76. },
  77. },
  78. data() {
  79. return {
  80. virtualListKey: u.getInstanceId(),
  81. virtualPageHeight: 0,
  82. virtualCellHeight: 0,
  83. virtualScrollTimeStamp: 0,
  84. virtualList: [],
  85. virtualPlaceholderTopHeight: 0,
  86. virtualPlaceholderBottomHeight: 0,
  87. virtualTopRangeIndex: 0,
  88. virtualBottomRangeIndex: 0,
  89. lastVirtualTopRangeIndex: 0,
  90. lastVirtualBottomRangeIndex: 0,
  91. virtualHeightCacheList: [],
  92. getCellHeightRetryCount: {
  93. fixed: 0,
  94. dynamic: 0
  95. },
  96. pagingOrgTop: -1,
  97. updateVirtualListFromDataChange: false
  98. }
  99. },
  100. watch: {
  101. realTotalData(newVal) {
  102. // #ifndef APP-NVUE
  103. if (this.finalUseVirtualList) {
  104. this.updateVirtualListFromDataChange = true;
  105. this.$nextTick(() => {
  106. if (!newVal.length) {
  107. this._resetDynamicListState(!this.isUserPullDown);
  108. }
  109. this.getCellHeightRetryCount.fixed = 0;
  110. newVal.length && this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight();
  111. this._updateVirtualScroll(this.oldScrollTop);
  112. })
  113. }
  114. // #endif
  115. },
  116. virtualList(newVal){
  117. this.$emit('update:virtualList', newVal);
  118. this.$emit('virtualListChange', newVal);
  119. }
  120. },
  121. computed: {
  122. finalUseVirtualList() {
  123. if (this.useVirtualList && this.usePageScroll){
  124. u.consoleErr('使用页面滚动时,开启虚拟列表无效!');
  125. }
  126. return this.useVirtualList && !this.usePageScroll;
  127. },
  128. finalUseInnerList() {
  129. return this.useInnerList || (this.finalUseVirtualList && !this.forceCloseInnerList)
  130. },
  131. finalCellKeyName() {
  132. // #ifdef APP-NVUE
  133. if (this.finalUseVirtualList && !this.cellKeyName.length){
  134. u.consoleErr('在nvue中开启use-virtual-list必须设置cell-key-name,否则将可能导致列表渲染错误!');
  135. }
  136. // #endif
  137. return this.cellKeyName;
  138. },
  139. finalVirtualPageHeight(){
  140. return this.virtualPageHeight > 0 ? this.virtualPageHeight : this.windowHeight;
  141. return virtualPageHeight * this.preloadPage;
  142. },
  143. virtualRangePageHeight(){
  144. return this.finalVirtualPageHeight * this.preloadPage;
  145. },
  146. virtualScrollDisTimeStamp() {
  147. return 1000 / this.virtualScrollFps;
  148. },
  149. },
  150. methods: {
  151. //在使用动态高度虚拟列表时,手动更新指定cell的缓存高度(当cell高度在初始化之后再次改变时调用),index代表需要更新的cell在列表中的位置,从0开始
  152. didUpdateVirtualListCell(index) {
  153. if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
  154. const currentNode = this.virtualHeightCacheList[index];
  155. this._getNodeClientRect(`#zp-id-${index}`,this.finalUseInnerList).then(cellNode => {
  156. const cellNodeHeight = cellNode ? cellNode[0].height : 0;
  157. const heightDis = cellNodeHeight - currentNode.height;
  158. currentNode.height = cellNodeHeight;
  159. currentNode.totalHeight = currentNode.lastHeight + cellNodeHeight;
  160. for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
  161. const thisNode = this.virtualHeightCacheList[i];
  162. if (i === index + 1) {
  163. thisNode.lastHeight = cellNodeHeight;
  164. }
  165. thisNode.totalHeight += heightDis;
  166. }
  167. });
  168. },
  169. //在使用动态高度虚拟列表时,若删除了列表数组中的某个item,需要调用此方法以更新高度缓存数组,index代表需要更新的cell在列表中的位置,从0开始
  170. didDeleteVirtualListCell(index) {
  171. if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
  172. const currentNode = this.virtualHeightCacheList[index];
  173. for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
  174. const thisNode = this.virtualHeightCacheList[i];
  175. if (i === index + 1) {
  176. thisNode.lastHeight = currentNode.lastHeight;
  177. }
  178. thisNode.totalHeight -= currentNode.height;
  179. }
  180. this.virtualHeightCacheList.splice(index, 1);
  181. },
  182. //初始化虚拟列表
  183. _virtualListInit() {
  184. this.$nextTick(() => {
  185. setTimeout(() => {
  186. this._getNodeClientRect('.zp-scroll-view').then(node => {
  187. if (node) {
  188. this.pagingOrgTop = node[0].top;
  189. this.virtualPageHeight = node[0].height;
  190. }
  191. });
  192. }, c.delayTime);
  193. })
  194. },
  195. //cellHeightMode为fixed时获取第一个cell高度
  196. _updateFixedCellHeight() {
  197. this.$nextTick(() => {
  198. const updateFixedCellHeightTimeout = setTimeout(() => {
  199. this._getNodeClientRect(`#zp-id-${0}`,this.finalUseInnerList).then(cellNode => {
  200. if (!cellNode) {
  201. clearTimeout(updateFixedCellHeightTimeout);
  202. if (this.getCellHeightRetryCount.fixed > 10) return;
  203. this.getCellHeightRetryCount.fixed ++;
  204. this._updateFixedCellHeight();
  205. } else {
  206. this.virtualCellHeight = cellNode[0].height;
  207. this._updateVirtualScroll(this.oldScrollTop);
  208. }
  209. });
  210. }, c.delayTime);
  211. })
  212. },
  213. //cellHeightMode为dynamic时获取每个cell高度
  214. _updateDynamicCellHeight(list) {
  215. this.$nextTick(() => {
  216. const updateDynamicCellHeightTimeout = setTimeout(async () => {
  217. for (let i = 0; i < list.length; i++) {
  218. let item = list[i];
  219. const cellNode = await this._getNodeClientRect(`#zp-id-${item[c.listCellIndexKey]}`,this.finalUseInnerList);
  220. const currentHeight = cellNode ? cellNode[0].height : 0;
  221. if (!cellNode) {
  222. clearTimeout(updateDynamicCellHeightTimeout);
  223. this.virtualHeightCacheList = this.virtualHeightCacheList.slice(-i);
  224. if (this.getCellHeightRetryCount.dynamic > 10) return;
  225. this.getCellHeightRetryCount.dynamic++;
  226. this._updateDynamicCellHeight(list);
  227. break;
  228. }
  229. const lastHeightCache = this.virtualHeightCacheList.length ? this.virtualHeightCacheList.slice(-1)[0] : null;
  230. const lastHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
  231. this.virtualHeightCacheList.push({
  232. height: currentHeight,
  233. lastHeight: lastHeight,
  234. totalHeight: lastHeight + currentHeight
  235. });
  236. }
  237. this._updateVirtualScroll(this.oldScrollTop);
  238. }, c.delayTime)
  239. })
  240. },
  241. //设置cellItem的index
  242. _setCellIndex(list, isFirstPage) {
  243. let lastItemIndex = 0;
  244. if (!isFirstPage) {
  245. lastItemIndex = this.realTotalData.length;
  246. const lastItem = this.realTotalData.length ? this.realTotalData.slice(-1)[0] : null;
  247. if (lastItem && lastItem[c.listCellIndexKey] !== undefined) {
  248. lastItemIndex = lastItem[c.listCellIndexKey] + 1;
  249. }
  250. } else {
  251. this._resetDynamicListState();
  252. }
  253. for (let i = 0; i < list.length; i++) {
  254. let item = list[i];
  255. if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
  256. item = {item};
  257. }
  258. item[c.listCellIndexKey] = lastItemIndex + i;
  259. item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[c.listCellIndexKey]}`;
  260. list[i] = item;
  261. }
  262. this.getCellHeightRetryCount.dynamic = 0;
  263. this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list);
  264. },
  265. //更新scroll滚动
  266. _updateVirtualScroll(scrollTop, scrollDiff = 0) {
  267. const currentTimeStamp = u.getTime();
  268. scrollTop === 0 && this._resetTopRange();
  269. if (scrollTop !== 0 && this.virtualScrollTimeStamp && currentTimeStamp - this.virtualScrollTimeStamp <= this.virtualScrollDisTimeStamp) {
  270. return;
  271. }
  272. this.virtualScrollTimeStamp = currentTimeStamp;
  273. let scrollIndex = 0;
  274. const cellHeightMode = this.cellHeightMode;
  275. if (cellHeightMode === Enum.CellHeightMode.Fixed) {
  276. scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
  277. this._updateFixedTopRangeIndex(scrollIndex);
  278. this._updateFixedBottomRangeIndex(scrollIndex);
  279. } else if(cellHeightMode === Enum.CellHeightMode.Dynamic) {
  280. const scrollDirection = scrollDiff > 0 ? 'top' : 'bottom';
  281. const rangePageHeight = this.virtualRangePageHeight;
  282. const topRangePageOffset = scrollTop - rangePageHeight;
  283. const bottomRangePageOffset = scrollTop + this.finalVirtualPageHeight + rangePageHeight;
  284. let virtualBottomRangeIndex = 0;
  285. let virtualPlaceholderBottomHeight = 0;
  286. let reachedLimitBottom = false;
  287. const heightCacheList = this.virtualHeightCacheList;
  288. const lastHeightCache = !!heightCacheList ? heightCacheList.slice(-1)[0] : null;
  289. let startTopRangeIndex = this.virtualTopRangeIndex;
  290. if (scrollDirection === 'bottom') {
  291. for (let i = startTopRangeIndex; i < heightCacheList.length;i++){
  292. const heightCacheItem = heightCacheList[i];
  293. if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
  294. this.virtualTopRangeIndex = i;
  295. this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
  296. break;
  297. }
  298. }
  299. } else {
  300. let topRangeMatched = false;
  301. for (let i = startTopRangeIndex; i >= 0;i--){
  302. const heightCacheItem = heightCacheList[i];
  303. if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
  304. this.virtualTopRangeIndex = i;
  305. this.virtualPlaceholderTopHeight = heightCacheItem.lastHeight;
  306. topRangeMatched = true;
  307. break;
  308. }
  309. }
  310. !topRangeMatched && this._resetTopRange();
  311. }
  312. for (let i = this.virtualTopRangeIndex; i < heightCacheList.length;i++){
  313. const heightCacheItem = heightCacheList[i];
  314. if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
  315. virtualBottomRangeIndex = i;
  316. virtualPlaceholderBottomHeight = lastHeightCache.totalHeight - heightCacheItem.totalHeight;
  317. reachedLimitBottom = true;
  318. break;
  319. }
  320. }
  321. if (!reachedLimitBottom || this.virtualBottomRangeIndex === 0) {
  322. this.virtualBottomRangeIndex = this.realTotalData.length ? this.realTotalData.length - 1 : this.pageSize;
  323. this.virtualPlaceholderBottomHeight = 0;
  324. } else {
  325. this.virtualBottomRangeIndex = virtualBottomRangeIndex;
  326. this.virtualPlaceholderBottomHeight = virtualPlaceholderBottomHeight;
  327. }
  328. this._updateVirtualList();
  329. }
  330. },
  331. //更新fixedCell模式下topRangeIndex&placeholderTopHeight
  332. _updateFixedTopRangeIndex(scrollIndex) {
  333. let virtualTopRangeIndex = this.virtualCellHeight === 0 ? 0 : scrollIndex - parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) * this.preloadPage;
  334. virtualTopRangeIndex *= this.virtualListCol;
  335. virtualTopRangeIndex = Math.max(0, virtualTopRangeIndex);
  336. this.virtualTopRangeIndex = virtualTopRangeIndex;
  337. this.virtualPlaceholderTopHeight = (virtualTopRangeIndex / this.virtualListCol) * this.virtualCellHeight;
  338. },
  339. //更新fixedCell模式下bottomRangeIndex&placeholderBottomHeight
  340. _updateFixedBottomRangeIndex(scrollIndex) {
  341. let virtualBottomRangeIndex = this.virtualCellHeight === 0 ? this.pageSize : scrollIndex + parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) * (this.preloadPage + 1);
  342. virtualBottomRangeIndex *= this.virtualListCol;
  343. virtualBottomRangeIndex = Math.min(this.realTotalData.length, virtualBottomRangeIndex);
  344. this.virtualBottomRangeIndex = virtualBottomRangeIndex;
  345. this.virtualPlaceholderBottomHeight = (this.realTotalData.length - virtualBottomRangeIndex) * this.virtualCellHeight / this.virtualListCol;
  346. this._updateVirtualList();
  347. },
  348. //更新virtualList
  349. _updateVirtualList() {
  350. const shouldUpdateList = this.updateVirtualListFromDataChange || (this.lastVirtualTopRangeIndex !== this.virtualTopRangeIndex || this.lastVirtualBottomRangeIndex !== this.virtualBottomRangeIndex);
  351. if (shouldUpdateList) {
  352. this.updateVirtualListFromDataChange = false;
  353. this.lastVirtualTopRangeIndex = this.virtualTopRangeIndex;
  354. this.lastVirtualBottomRangeIndex = this.virtualBottomRangeIndex;
  355. this.virtualList = this.realTotalData.slice(this.virtualTopRangeIndex, this.virtualBottomRangeIndex + 1);
  356. }
  357. },
  358. //重置动态cell模式下的高度缓存数据、虚拟列表和滚动状态
  359. _resetDynamicListState(resetVirtualList = false) {
  360. this.virtualHeightCacheList = [];
  361. if (resetVirtualList) {
  362. this.virtualList = [];
  363. }
  364. this.virtualTopRangeIndex = 0;
  365. this.virtualPlaceholderTopHeight = 0;
  366. },
  367. //重置topRangeIndex和placeholderTopHeight
  368. _resetTopRange() {
  369. this.virtualTopRangeIndex = 0;
  370. this.virtualPlaceholderTopHeight = 0;
  371. this._updateVirtualList();
  372. },
  373. //检测虚拟列表当前滚动位置,如发现滚动位置不正确则重新计算虚拟列表相关参数(为解决在App中可能出现的长时间进入后台后打开App白屏的问题)
  374. _checkVirtualListScroll() {
  375. if (this.finalUseVirtualList) {
  376. this.$nextTick(() => {
  377. this._getNodeClientRect('.zp-paging-touch-view').then(node => {
  378. const currentTop = node ? node[0].top : 0;
  379. if (!node || (currentTop === this.pagingOrgTop && this.virtualPlaceholderTopHeight !== 0)) {
  380. this._updateVirtualScroll(0);
  381. }
  382. });
  383. })
  384. }
  385. },
  386. //处理使用内置列表时点击了cell事件
  387. _innerCellClick(item, index) {
  388. this.$emit('innerCellClick', item, index);
  389. }
  390. }
  391. }