diff --git a/packages/vue-virtual-scroller/src/components/RecycleScroller.vue b/packages/vue-virtual-scroller/src/components/RecycleScroller.vue index 64fe2160..a00e69b9 100644 --- a/packages/vue-virtual-scroller/src/components/RecycleScroller.vue +++ b/packages/vue-virtual-scroller/src/components/RecycleScroller.vue @@ -186,8 +186,10 @@ export default { created () { this.$_startIndex = 0 this.$_endIndex = 0 + // Visible views by their key this.$_views = new Map() - this.$_unusedViews = new Map() + // Pools of recycled views, by view type + this.$_recycledPools = new Map() this.$_scrollDirty = false this.$_lastUpdateScrollPosition = 0 @@ -214,7 +216,17 @@ export default { }, methods: { - addView (pool, index, item, key, type) { + getRecycledPool (type) { + const recycledPools = this.$_recycledPools + let recycledPool = recycledPools.get(type) + if (!recycledPool) { + recycledPool = [] + recycledPools.set(type, recycledPool) + } + return recycledPool + }, + + createView (pool, index, item, key, type) { const nr = markRaw({ id: uid++, index, @@ -231,19 +243,31 @@ export default { return view }, - unuseView (view, fake = false) { - const unusedViews = this.$_unusedViews - const type = view.nr.type - let unusedPool = unusedViews.get(type) - if (!unusedPool) { - unusedPool = [] - unusedViews.set(type, unusedPool) + getRecycledView (type) { + const recycledPool = this.getRecycledPool(type) + if (recycledPool && recycledPool.length) { + const view = recycledPool.pop() + view.nr.used = true + return view + } else { + return null } - unusedPool.push(view) - if (!fake) { - view.nr.used = false - view.position = -9999 - this.$_views.delete(view.nr.key) + }, + + removeAndRecycleView (view) { + const type = view.nr.type + const recycledPool = this.getRecycledPool(type) + recycledPool.push(view) + view.nr.used = false + view.position = -9999 + this.$_views.delete(view.nr.key) + }, + + removeAndRecycleAllViews () { + this.$_views.clear() + this.$_recycledPools.clear() + for (let i = 0, l = this.pool.length; i < l; i++) { + this.removeAndRecycleView(this.pool[i]) } }, @@ -282,7 +306,7 @@ export default { } }, - updateVisibleItems (checkItem, checkPositionDiff = false) { + updateVisibleItems (itemsChanged, checkPositionDiff = false) { const itemSize = this.itemSize const minItemSize = this.$_computedMinItemSize const typeField = this.typeField @@ -291,7 +315,6 @@ export default { const count = items.length const sizes = this.sizes const views = this.$_views - const unusedViews = this.$_unusedViews const pool = this.pool let startIndex, endIndex let totalSize @@ -378,98 +401,56 @@ export default { const continuous = startIndex <= this.$_endIndex && endIndex >= this.$_startIndex - if (this.$_continuous !== continuous) { - if (continuous) { - views.clear() - unusedViews.clear() - for (let i = 0, l = pool.length; i < l; i++) { - view = pool[i] - this.unuseView(view) - } - } - this.$_continuous = continuous - } else if (continuous) { + // Step 1: Mark any invisible elements as unused + if (!continuous || itemsChanged) { + this.removeAndRecycleAllViews() + } else { for (let i = 0, l = pool.length; i < l; i++) { view = pool[i] if (view.nr.used) { - // Update view item index - if (checkItem) { - view.nr.index = items.findIndex( - item => keyField ? item[keyField] === view.item[keyField] : item === view.item, - ) - } - - // Check if index is still in visible range - if ( - view.nr.index === -1 || - view.nr.index < startIndex || - view.nr.index >= endIndex - ) { - this.unuseView(view) + const viewVisible = view.nr.index >= startIndex && view.nr.index < endIndex + const viewSize = itemSize || sizes[i].size + if (!viewVisible || !viewSize) { + this.removeAndRecycleView(view) } } } } - const unusedIndex = continuous ? null : new Map() - - let item, type, unusedPool - let v + // Step 2: Assign a view and update props for every view that became visible + let item, type for (let i = startIndex; i < endIndex; i++) { + const elementSize = itemSize || sizes[i].size + if (!elementSize) continue item = items[i] - const key = keyField ? item[keyField] : item + const key = keyField ? item[keyField] : i if (key == null) { throw new Error(`Key is ${key} on item (keyField is '${keyField}')`) } view = views.get(key) - if (!itemSize && !sizes[i].size) { - if (view) this.unuseView(view) - continue - } - - // No view assigned to item if (!view) { + // Item just became visible type = item[typeField] - unusedPool = unusedViews.get(type) - - if (continuous) { - // Reuse existing view - if (unusedPool && unusedPool.length) { - view = unusedPool.pop() - view.item = item - view.nr.used = true - view.nr.index = i - view.nr.key = key - view.nr.type = type - } else { - view = this.addView(pool, i, item, key, type) - } - } else { - // Use existing view - // We don't care if they are already used - // because we are not in continous scrolling - v = unusedIndex.get(type) || 0 - - if (!unusedPool || v >= unusedPool.length) { - view = this.addView(pool, i, item, key, type) - this.unuseView(view, true) - unusedPool = unusedViews.get(type) - } + view = this.getRecycledView(type) - view = unusedPool[v] + if (view) { view.item = item - view.nr.used = true view.nr.index = i view.nr.key = key - view.nr.type = type - unusedIndex.set(type, v + 1) - v++ + if (view.nr.type !== type) { + console.warn("Reused view's type does not match pool's type") + } + } else { + // No recycled view available, create a new one + view = this.createView(pool, i, item, key, type) } views.set(key, view) } else { - view.nr.used = true - view.item = item + if (view.item !== item) { view.item = item } + if (!view.nr.used) { + console.warn("Expected existing view's used flag to be true, got " + view.nr.used) + } } // Update position @@ -596,7 +577,7 @@ export default { }, sortViews () { - this.pool.sort((viewA, viewB) => viewA.index - viewB.index) + this.pool.sort((viewA, viewB) => viewA.nr.index - viewB.nr.index) }, }, }