UNPKG

@shopify/flash-list

Version:

FlashList is a more performant FlatList replacement

343 lines 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RenderStackManager = void 0; var tslib_1 = require("tslib"); /** * Manages the recycling of rendered items in a virtualized list. * This class handles tracking, recycling, and reusing item keys to optimize * rendering performance by minimizing creation/destruction of components. */ var RenderStackManager = /** @class */ (function () { /** * @param maxItemsInRecyclePool - Maximum number of items that can be in the recycle pool */ function RenderStackManager(maxItemsInRecyclePool) { if (maxItemsInRecyclePool === void 0) { maxItemsInRecyclePool = Number.MAX_SAFE_INTEGER; } this.disableRecycling = false; this.maxItemsInRecyclePool = maxItemsInRecyclePool; this.recycleKeyPools = new Map(); this.keyMap = new Map(); this.stableIdMap = new Map(); this.keyCounter = 0; this.unProcessedIndices = new Set(); } /** * Synchronizes the render stack with the current state of data. * This method is the core orchestrator that: * 1. Recycles keys for items that are no longer valid * 2. Updates existing keys for items that remain visible * 3. Assigns new keys for newly visible items * 4. Cleans up excess items to maintain the recycling pool size * * @param getStableId - Function to get a stable identifier for an item at a specific index * @param getItemType - Function to get the type of an item at a specific index * @param engagedIndices - Collection of indices that are currently visible or engaged * @param dataLength - Total length of the data set */ RenderStackManager.prototype.sync = function (getStableId, getItemType, engagedIndices, dataLength) { var e_1, _a, e_2, _b, e_3, _c, e_4, _d, e_5, _e; var _this = this; this.clearRecyclePool(); this.unProcessedIndices.clear(); // Recycle keys for items that are no longer valid or visible this.keyMap.forEach(function (keyInfo, key) { var index = keyInfo.index, stableId = keyInfo.stableId, itemType = keyInfo.itemType; if (index >= dataLength) { _this.recycleKey(key); return; } if (!_this.disableRecycling) { _this.unProcessedIndices.add(index); } if (!engagedIndices.includes(index)) { _this.recycleKey(key); return; } var newStableId = getStableId(index); var newItemType = getItemType(index); if (stableId !== newStableId || itemType !== newItemType) { _this.recycleKey(key); } }); try { // First pass: process items that already have optimized keys for (var engagedIndices_1 = tslib_1.__values(engagedIndices), engagedIndices_1_1 = engagedIndices_1.next(); !engagedIndices_1_1.done; engagedIndices_1_1 = engagedIndices_1.next()) { var index = engagedIndices_1_1.value; if (this.hasOptimizedKey(getStableId(index))) { this.syncItem(index, getItemType(index), getStableId(index)); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (engagedIndices_1_1 && !engagedIndices_1_1.done && (_a = engagedIndices_1.return)) _a.call(engagedIndices_1); } finally { if (e_1) throw e_1.error; } } try { // Second pass: process remaining items that need new keys for (var engagedIndices_2 = tslib_1.__values(engagedIndices), engagedIndices_2_1 = engagedIndices_2.next(); !engagedIndices_2_1.done; engagedIndices_2_1 = engagedIndices_2.next()) { var index = engagedIndices_2_1.value; if (!this.hasOptimizedKey(getStableId(index))) { this.syncItem(index, getItemType(index), getStableId(index)); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (engagedIndices_2_1 && !engagedIndices_2_1.done && (_b = engagedIndices_2.return)) _b.call(engagedIndices_2); } finally { if (e_2) throw e_2.error; } } // create indices that are not in the engagedIndices and less than dataLength // select only indices that are not in the engagedIndices var validIndicesInPool = []; try { for (var _f = tslib_1.__values(this.keyMap.values()), _g = _f.next(); !_g.done; _g = _f.next()) { var keyInfo = _g.value; var index = keyInfo.index; if (index < dataLength && !engagedIndices.includes(index)) { validIndicesInPool.push(index); } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_g && !_g.done && (_c = _f.return)) _c.call(_f); } finally { if (e_3) throw e_3.error; } } try { // First pass: process items that already have optimized keys for (var validIndicesInPool_1 = tslib_1.__values(validIndicesInPool), validIndicesInPool_1_1 = validIndicesInPool_1.next(); !validIndicesInPool_1_1.done; validIndicesInPool_1_1 = validIndicesInPool_1.next()) { var index = validIndicesInPool_1_1.value; if (this.hasOptimizedKey(getStableId(index))) { this.syncItem(index, getItemType(index), getStableId(index)); } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (validIndicesInPool_1_1 && !validIndicesInPool_1_1.done && (_d = validIndicesInPool_1.return)) _d.call(validIndicesInPool_1); } finally { if (e_4) throw e_4.error; } } try { for (var validIndicesInPool_2 = tslib_1.__values(validIndicesInPool), validIndicesInPool_2_1 = validIndicesInPool_2.next(); !validIndicesInPool_2_1.done; validIndicesInPool_2_1 = validIndicesInPool_2.next()) { var index = validIndicesInPool_2_1.value; if (!this.hasOptimizedKey(getStableId(index))) { this.syncItem(index, getItemType(index), getStableId(index)); } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (validIndicesInPool_2_1 && !validIndicesInPool_2_1.done && (_e = validIndicesInPool_2.return)) _e.call(validIndicesInPool_2); } finally { if (e_5) throw e_5.error; } } // Clean up stale items and manage the recycle pool size this.cleanup(getStableId, getItemType, engagedIndices, dataLength); }; /** * Checks if a stable ID already has an assigned key */ RenderStackManager.prototype.hasOptimizedKey = function (stableId) { return this.stableIdMap.has(stableId); }; /** * Cleans up stale keys and manages the recycle pool size. * This ensures we don't maintain references to items that are no longer in the dataset, * and limits the number of recycled items to avoid excessive memory usage. */ RenderStackManager.prototype.cleanup = function (getStableId, getItemType, engagedIndices, dataLength) { var e_6, _a, e_7, _b; var itemsToDelete = new Array(); try { // Remove items that are no longer in the dataset for (var _c = tslib_1.__values(this.keyMap.entries()), _d = _c.next(); !_d.done; _d = _c.next()) { var _e = tslib_1.__read(_d.value, 2), key = _e[0], keyInfo = _e[1]; var index = keyInfo.index, itemType = keyInfo.itemType, stableId = keyInfo.stableId; var indexOutOfBounds = index >= dataLength; var hasStableIdChanged = !indexOutOfBounds && getStableId(index) !== stableId; if (indexOutOfBounds || hasStableIdChanged) { var nextIndex = this.unProcessedIndices.values().next().value; var shouldDeleteKey = true; if (nextIndex !== undefined) { var nextItemType = getItemType(nextIndex); var nextStableId = getStableId(nextIndex); if (itemType === nextItemType) { this.syncItem(nextIndex, nextItemType, nextStableId); shouldDeleteKey = false; } } if (shouldDeleteKey) { this.deleteKeyFromRecyclePool(itemType, key); this.stableIdMap.delete(stableId); itemsToDelete.push(key); } } } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_6) throw e_6.error; } } try { for (var itemsToDelete_1 = tslib_1.__values(itemsToDelete), itemsToDelete_1_1 = itemsToDelete_1.next(); !itemsToDelete_1_1.done; itemsToDelete_1_1 = itemsToDelete_1.next()) { var key = itemsToDelete_1_1.value; this.keyMap.delete(key); } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (itemsToDelete_1_1 && !itemsToDelete_1_1.done && (_b = itemsToDelete_1.return)) _b.call(itemsToDelete_1); } finally { if (e_7) throw e_7.error; } } // Limit the size of the recycle pool var itemsRenderedForRecycling = this.keyMap.size - engagedIndices.length; if (itemsRenderedForRecycling > this.maxItemsInRecyclePool) { var deleteCount = itemsRenderedForRecycling - this.maxItemsInRecyclePool; var deleted = 0; // Use a for loop so we can break early once we've deleted enough items var entries = Array.from(this.keyMap.entries()).reverse(); for (var i = 0; i < entries.length && deleted < deleteCount; i++) { var _f = tslib_1.__read(entries[i], 2), key = _f[0], keyInfo = _f[1]; var index = keyInfo.index, itemType = keyInfo.itemType, stableId = keyInfo.stableId; if (!engagedIndices.includes(index)) { this.deleteKeyFromRecyclePool(itemType, key); this.stableIdMap.delete(stableId); this.keyMap.delete(key); deleted++; } } } }; /** * Places a key back into its type-specific recycle pool for future reuse */ RenderStackManager.prototype.recycleKey = function (key) { if (this.disableRecycling) { return; } var keyInfo = this.keyMap.get(key); if (!keyInfo) { return; } var itemType = keyInfo.itemType; // Add key back to its type's pool var pool = this.getRecyclePoolForType(itemType); pool.add(key); }; /** * Returns the current render stack containing all active keys and their metadata */ RenderStackManager.prototype.getRenderStack = function () { return this.keyMap; }; /** * Syncs an individual item by assigning it an appropriate key. * Will use an existing key if available, or generate a new one. * * @returns The key assigned to the item */ RenderStackManager.prototype.syncItem = function (index, itemType, stableId) { // Try to reuse an existing key, or get one from the recycle pool, or generate a new one var newKey = this.stableIdMap.get(stableId) || this.getKeyFromRecyclePool(itemType) || this.generateKey(); this.unProcessedIndices.delete(index); var keyInfo = this.keyMap.get(newKey); if (keyInfo) { // Update an existing key's metadata this.deleteKeyFromRecyclePool(itemType, newKey); this.deleteKeyFromRecyclePool(keyInfo.itemType, newKey); this.stableIdMap.delete(keyInfo.stableId); keyInfo.index = index; keyInfo.itemType = itemType; keyInfo.stableId = stableId; } else { // Create a new entry in the key map this.keyMap.set(newKey, { itemType: itemType, index: index, stableId: stableId, }); } this.stableIdMap.set(stableId, newKey); return newKey; }; /** * Clears all recycled keys from the pool, effectively resetting the recycling system. * This operation does not affect currently active keys. */ RenderStackManager.prototype.clearRecyclePool = function () { var e_8, _a; try { // iterate over all pools and clear them for (var _b = tslib_1.__values(this.recycleKeyPools.values()), _c = _b.next(); !_c.done; _c = _b.next()) { var pool = _c.value; pool.clear(); } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_8) throw e_8.error; } } }; /** * Generates a unique sequential key using an internal counter. * @returns A unique key as a string */ RenderStackManager.prototype.generateKey = function () { return (this.keyCounter++).toString(); }; /** * Removes a specific key from its type's recycle pool */ RenderStackManager.prototype.deleteKeyFromRecyclePool = function (itemType, key) { var _a; (_a = this.recycleKeyPools.get(itemType)) === null || _a === void 0 ? void 0 : _a.delete(key); }; /** * Gets or creates a recycle pool for a specific item type */ RenderStackManager.prototype.getRecyclePoolForType = function (itemType) { var pool = this.recycleKeyPools.get(itemType); if (!pool) { pool = new Set(); this.recycleKeyPools.set(itemType, pool); } return pool; }; /** * Retrieves and removes a key from the type's recycle pool * @returns A recycled key or undefined if none available */ RenderStackManager.prototype.getKeyFromRecyclePool = function (itemType) { var pool = this.getRecyclePoolForType(itemType); if (pool.size > 0) { var key = pool.values().next().value; pool.delete(key); return key; } return undefined; }; return RenderStackManager; }()); exports.RenderStackManager = RenderStackManager; //# sourceMappingURL=RenderStackManager.js.map