UNPKG

@blueprintjs/table

Version:

Scalable interactive table component

217 lines 7.8 kB
/* * Copyright 2017 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { requestIdleCallback } from "./requestIdleCallback"; /** * This class helps batch updates to large lists. * * For example, if your React component has many children, updating them all at * once may cause jank when reconciling the DOM. This class helps you update * only a few children per frame. * * A typical usage would be: * * ```tsx * public renderChildren = (allChildrenKeys: string[]) => { * * batcher.startNewBatch(); * * allChildrenKeys.forEach((prop1: string, index: number) => { * batcher.addArgsToBatch(prop1, "prop2", index); * }); * * batcher.removeOldAddNew((prop1: string, prop2: string, other: number) => { * return <Child prop1={prop1} prop2={prop2} other={other} />; * }); * * if (!batcher.isDone()) { * batcher.idleCallback(this.forceUpdate()); * } * * const currentChildren = batcher.getList(); * return currentChildren; * } * * ``` */ export class Batcher { constructor() { this.currentObjects = {}; this.oldObjects = {}; this.batchArgs = {}; this.done = true; this.handleIdleCallback = () => { const callback = this.callback; delete this.callback; callback === null || callback === void 0 ? void 0 : callback(); }; this.mapCurrentObjectKey = (key) => { return this.currentObjects[key]; }; } /** * Resets the "batch" and "current" sets. This essentially clears the cache * and prevents accidental re-use of "current" objects. */ reset() { this.batchArgs = {}; this.oldObjects = this.currentObjects; this.currentObjects = {}; } /** * Starts a new "batch" argument set */ startNewBatch() { this.batchArgs = {}; } /** * Stores the variadic arguments to be later batched together. * * The arguments must be simple stringifyable objects. */ addArgsToBatch(...args) { this.batchArgs[this.getKey(args)] = args; } /** * Compares the set of "batch" arguments to the "current" set. Creates any * new objects using the callback as a factory. Removes old objects. * * Arguments that are in the "current" set but were not part of the last * "batch" set are considered candidates for removal. Similarly, Arguments * that are part of the "batch" set but not the "current" set are candidates * for addition. * * The number of objects added and removed may be limited with the * `...Limit` parameters. * * Finally, the batcher determines if the batching is complete if the * "current" arguments match the "batch" arguments. */ removeOldAddNew(callback, addNewLimit = Batcher.DEFAULT_ADD_LIMIT, removeOldLimit = Batcher.DEFAULT_REMOVE_LIMIT, updateLimit = Batcher.DEFAULT_UPDATE_LIMIT) { // remove old const keysToRemove = this.setKeysDifference(this.currentObjects, this.batchArgs, removeOldLimit); keysToRemove.forEach(key => delete this.currentObjects[key]); // remove ALL old objects not in batch const keysToRemoveOld = this.setKeysDifference(this.oldObjects, this.batchArgs, -1); keysToRemoveOld.forEach(key => delete this.oldObjects[key]); // copy ALL old objects into current objects if not defined const keysToShallowCopy = Object.keys(this.oldObjects); keysToShallowCopy.forEach(key => { if (this.currentObjects[key] == null) { this.currentObjects[key] = this.oldObjects[key]; } }); // update old objects with factory const keysToUpdate = this.setKeysIntersection(this.oldObjects, this.currentObjects, updateLimit); keysToUpdate.forEach(key => { delete this.oldObjects[key]; this.currentObjects[key] = callback.apply(undefined, this.batchArgs[key]); }); // add new objects with factory const keysToAdd = this.setKeysDifference(this.batchArgs, this.currentObjects, addNewLimit); keysToAdd.forEach(key => (this.currentObjects[key] = callback.apply(undefined, this.batchArgs[key]))); // set `done` to true if sets match exactly after add/remove and there // are no "old objects" remaining this.done = this.setHasSameKeys(this.batchArgs, this.currentObjects) && Object.keys(this.oldObjects).length === 0; } /** * Returns true if the "current" set matches the "batch" set. */ isDone() { return this.done; } /** * Returns all the objects in the "current" set. */ getList() { return Object.keys(this.currentObjects).map(this.mapCurrentObjectKey); } /** * Registers a callback to be invoked on the next idle frame. If a callback * has already been registered, we do not register a new one. */ idleCallback(callback) { if (!this.callback) { this.callback = callback; requestIdleCallback(this.handleIdleCallback); } } cancelOutstandingCallback() { delete this.callback; } /** * Forcibly overwrites the current list of batched objects. Not recommended * for normal usage. */ setList(objectsArgs, objects) { this.reset(); objectsArgs.forEach((args, i) => { this.addArgsToBatch(...args); this.currentObjects[this.getKey(args)] = objects[i]; }); this.done = true; } getKey(args) { return args.join(Batcher.ARG_DELIMITER); } setKeysDifference(a, b, limit) { return this.setKeysOperation(a, b, "difference", limit); } setKeysIntersection(a, b, limit) { return this.setKeysOperation(a, b, "intersect", limit); } /** * Compares the keys of A from B -- and performs an "intersection" or * "difference" operation on the keys. * * Note that the order of operands A and B matters for the "difference" * operation. * * Returns an array of at most `limit` keys. */ setKeysOperation(a, b, operation, limit) { const result = []; const aKeys = Object.keys(a); for (let i = 0; i < aKeys.length && (limit < 0 || result.length < limit); i++) { const key = aKeys[i]; if ((operation === "difference" && a[key] && !b[key]) || (operation === "intersect" && a[key] && b[key])) { result.push(key); } } return result; } /** * Returns true of objects `a` and `b` have exactly the same keys. */ setHasSameKeys(a, b) { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) { return false; } for (const aKey of aKeys) { if (b[aKey] === undefined) { return false; } } return true; } } Batcher.DEFAULT_ADD_LIMIT = 20; Batcher.DEFAULT_UPDATE_LIMIT = 20; Batcher.DEFAULT_REMOVE_LIMIT = 20; Batcher.ARG_DELIMITER = "|"; //# sourceMappingURL=batcher.js.map