Last active
September 4, 2018 09:37
-
-
Save niieani/af08bd464f72a809d07639b2d9f88a38 to your computer and use it in GitHub Desktop.
Aurelia: Suboptimal repeat element lifecycle [alternative without compose + optimized-repeat]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <require from="./component"></require> | |
| <input type="checkbox" ref="isFiltering"> | |
| <br> | |
| <component | |
| optimized-repeat.for="id of components | without8: isFiltering.checked" | |
| text.bind="id"> | |
| [${$index}] | |
| </component> | |
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {Component} from './component' | |
| export class App { | |
| constructor() { | |
| this.components = [] | |
| for (let i = 1; i <= 10; i++) { | |
| this.components.push(i) | |
| } | |
| } | |
| } | |
| export class Without8ValueConverter { | |
| toView(array, without) { | |
| if (without) { | |
| return array.filter(element => element !== 8) | |
| } | |
| return array | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {ArrayRepeatStrategy} from 'aurelia-templating-resources/array-repeat-strategy'; | |
| import {createFullOverrideContext, updateOverrideContexts} from 'aurelia-templating-resources/repeat-utilities'; | |
| import {mergeSplice} from 'aurelia-binding'; | |
| import {updateOverrideContext} from 'aurelia-templating-resources/repeat-utilities'; | |
| /** | |
| * A strategy for repeating a template over an array. | |
| */ | |
| export class ArrayOptimizedRepeatStrategy extends ArrayRepeatStrategy { | |
| /** | |
| * Gets an observer for the specified collection. | |
| * @param observerLocator The observer locator instance. | |
| * @param items The items to be observed. | |
| */ | |
| getCollectionObserver(observerLocator, items) { | |
| return observerLocator.getArrayObserver(items); | |
| } | |
| /** | |
| * Handle the repeat's collection instance changing. | |
| * @param repeat The repeater instance. | |
| * @param items The new array instance. | |
| */ | |
| instanceChanged(repeat, items) { | |
| // if the new instance does not contain any items, | |
| // just remove all views and don't do any further processing | |
| if (!items || items.length === 0) { | |
| let removePromise = repeat.removeAllViews(true); | |
| if (removePromise instanceof Promise) { | |
| removePromise.then(() => this._standardProcessInstanceChanged(repeat, items)); | |
| } | |
| return; | |
| } | |
| const itemNameInBindingContext = repeat.local; | |
| const children = repeat.views(); | |
| const childrenSnapshot = children.slice(0); | |
| const viewsLength = children.length; | |
| const itemsLength = items.length; | |
| // the cache of the current state (it will be transformed along with the views to keep track of indicies) | |
| let itemsPreviouslyInViews = []; | |
| const rmPromises = []; | |
| for (let index = 0; index < viewsLength; index++) { | |
| const oldItem = childrenSnapshot[index].bindingContext[itemNameInBindingContext]; | |
| if (items.indexOf(oldItem) === -1) { | |
| // remove the item if no longer in the new instance of items | |
| const viewOrPromise = repeat.removeView(index, true); | |
| if (viewOrPromise instanceof Promise) { | |
| rmPromises.push(viewOrPromise); | |
| } | |
| } else { | |
| // or add the item to the cache list | |
| itemsPreviouslyInViews.push(oldItem); | |
| } | |
| } | |
| if (itemsPreviouslyInViews.length > 0) { | |
| Promise.all(rmPromises).then(() => { | |
| // update views (create new and move existing) | |
| for (let index = 0; index < itemsLength; index++) { | |
| const item = items[index]; | |
| const indexOfView = itemsPreviouslyInViews.indexOf(item); | |
| let view; | |
| if (indexOfView === -1) { // create views for new items | |
| const overrideContext = createFullOverrideContext(repeat, items[index], index, itemsLength); | |
| repeat.insertView(index, overrideContext.bindingContext, overrideContext); | |
| // reflect the change in our cache list so indicies are valid | |
| itemsPreviouslyInViews.splice(index, 0, undefined); | |
| } | |
| else if (indexOfView === index) { // leave unchanged items | |
| view = children[indexOfView]; | |
| itemsPreviouslyInViews[indexOfView] = undefined; | |
| } | |
| else { // move the element to the right place | |
| view = children[indexOfView]; | |
| repeat.moveView(indexOfView, index); | |
| itemsPreviouslyInViews.splice(indexOfView, 1); | |
| itemsPreviouslyInViews.splice(index, 0, undefined); | |
| } | |
| if (view) { | |
| updateOverrideContext(view.overrideContext, index, itemsLength); | |
| } | |
| } | |
| // fix ordering and remove extraneous elements in case of duplicates | |
| this._inPlaceProcessItems(repeat, items); | |
| }) | |
| } | |
| else { | |
| // this is either the first time an instance was assigned to this repeat | |
| // or there were no views created yet | |
| if (repeat.viewsRequireLifecycle) { | |
| this._standardProcessInstanceChanged(repeat, items); | |
| return; | |
| } | |
| this._inPlaceProcessItems(repeat, items); | |
| } | |
| } | |
| _standardProcessInstanceChanged(repeat, items) { | |
| for (let i = 0, ii = items.length; i < ii; i++) { | |
| let overrideContext = createFullOverrideContext(repeat, items[i], i, ii); | |
| repeat.addView(overrideContext.bindingContext, overrideContext); | |
| } | |
| } | |
| _inPlaceProcessItems(repeat, items) { | |
| let itemsLength = items.length; | |
| let viewsLength = repeat.viewCount(); | |
| // remove unneeded views. | |
| while (viewsLength > itemsLength) { | |
| viewsLength--; | |
| repeat.removeView(viewsLength, true); | |
| } | |
| // avoid repeated evaluating the property-getter for the "local" property. | |
| let local = repeat.local; | |
| // re-evaluate bindings on existing views. | |
| for (let i = 0; i < viewsLength; i++) { | |
| let view = repeat.view(i); | |
| let last = i === itemsLength - 1; | |
| let middle = i !== 0 && !last; | |
| // any changes to the binding context? | |
| if (view.bindingContext[local] === items[i] | |
| && view.overrideContext.$middle === middle | |
| && view.overrideContext.$last === last) { | |
| // no changes. continue... | |
| continue; | |
| } | |
| // update the binding context and refresh the bindings. | |
| view.bindingContext[local] = items[i]; | |
| view.overrideContext.$middle = middle; | |
| view.overrideContext.$last = last; | |
| repeat.updateBindings(view); | |
| } | |
| // add new views | |
| for (let i = viewsLength; i < itemsLength; i++) { | |
| let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); | |
| repeat.addView(overrideContext.bindingContext, overrideContext); | |
| } | |
| } | |
| /** | |
| * Handle the repeat's collection instance mutating. | |
| * @param repeat The repeat instance. | |
| * @param array The modified array. | |
| * @param splices Records of array changes. | |
| */ | |
| instanceMutated(repeat, array, splices) { | |
| if (repeat.viewsRequireLifecycle) { | |
| this._standardProcessInstanceMutated(repeat, array, splices); | |
| return; | |
| } | |
| this._inPlaceProcessItems(repeat, array); | |
| } | |
| _standardProcessInstanceMutated(repeat, array, splices) { | |
| if (repeat.__queuedSplices) { | |
| for (let i = 0, ii = splices.length; i < ii; ++i) { | |
| let {index, removed, addedCount} = splices[i]; | |
| mergeSplice(repeat.__queuedSplices, index, removed, addedCount); | |
| } | |
| // Array.prototype.slice is used here to clone the array | |
| repeat.__array = array.slice(0); | |
| return; | |
| } | |
| // Array.prototype.slice is used here to clone the array | |
| let maybePromise = this._runSplices(repeat, array.slice(0), splices); | |
| if (maybePromise instanceof Promise) { | |
| let queuedSplices = repeat.__queuedSplices = []; | |
| let runQueuedSplices = () => { | |
| if (!queuedSplices.length) { | |
| repeat.__queuedSplices = undefined; | |
| repeat.__array = undefined; | |
| return; | |
| } | |
| let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); | |
| queuedSplices = repeat.__queuedSplices = []; | |
| nextPromise.then(runQueuedSplices); | |
| }; | |
| maybePromise.then(runQueuedSplices); | |
| } | |
| } | |
| /** | |
| * Run a normalised set of splices against the viewSlot children. | |
| * @param repeat The repeat instance. | |
| * @param array The modified array. | |
| * @param splices Records of array changes. | |
| * @return {Promise|undefined} A promise if animations have to be run. | |
| * @pre The splices must be normalised so as: | |
| * * Any item added may not be later removed. | |
| * * Removals are ordered by asending index | |
| */ | |
| _runSplices(repeat, array, splices) { | |
| let removeDelta = 0; | |
| let rmPromises = []; | |
| for (let i = 0, ii = splices.length; i < ii; ++i) { | |
| let splice = splices[i]; | |
| let removed = splice.removed; | |
| for (let j = 0, jj = removed.length; j < jj; ++j) { | |
| // the rmPromises.length correction works due to the ordered removal precondition | |
| let viewOrPromise = repeat.removeView(splice.index + removeDelta + rmPromises.length, true); | |
| if (viewOrPromise instanceof Promise) { | |
| rmPromises.push(viewOrPromise); | |
| } | |
| } | |
| removeDelta -= splice.addedCount; | |
| } | |
| if (rmPromises.length > 0) { | |
| return Promise.all(rmPromises).then(() => { | |
| let spliceIndexLow = this._handleAddedSplices(repeat, array, splices); | |
| updateOverrideContexts(repeat.views(), spliceIndexLow); | |
| }); | |
| } | |
| let spliceIndexLow = this._handleAddedSplices(repeat, array, splices); | |
| updateOverrideContexts(repeat.views(), spliceIndexLow); | |
| } | |
| _handleAddedSplices(repeat, array, splices) { | |
| let spliceIndex; | |
| let spliceIndexLow; | |
| let arrayLength = array.length; | |
| for (let i = 0, ii = splices.length; i < ii; ++i) { | |
| let splice = splices[i]; | |
| let addIndex = spliceIndex = splice.index; | |
| let end = splice.index + splice.addedCount; | |
| if (typeof spliceIndexLow === 'undefined' || spliceIndexLow === null || spliceIndexLow > splice.index) { | |
| spliceIndexLow = spliceIndex; | |
| } | |
| for (; addIndex < end; ++addIndex) { | |
| let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); | |
| repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); | |
| } | |
| } | |
| return spliceIndexLow; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {inlineView, bindable} from 'aurelia-framework' | |
| @inlineView('<template><content></content>${text}<br></template>') | |
| export class Component { | |
| @bindable text | |
| attached() { | |
| console.log('attached', this.text) | |
| } | |
| bind() { | |
| console.log('bind', this.text) | |
| } | |
| unbind() { | |
| console.log('unbind', this.text) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html> | |
| <head> | |
| <title>Aurelia</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| </head> | |
| <body> | |
| <h1>Loading...</h1> | |
| <script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script> | |
| <script src="https://jdanyow.github.io/rjs-bundle/config.js"></script> | |
| <script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script> | |
| <script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script> | |
| <script> | |
| require(['./main']); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {bootstrap} from 'aurelia-bootstrapper'; | |
| bootstrap((aurelia: Aurelia): void => { | |
| aurelia.use | |
| .standardConfiguration() | |
| .developmentLogging(); | |
| aurelia.use.plugin('optimized-repeat-index'); | |
| aurelia.start().then(() => aurelia.setRoot('app', document.body)); | |
| }); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {OptimizedRepeat} from './optimized-repeat'; | |
| import {ViewSlot} from 'aurelia-templating'; | |
| export function configure(config) { | |
| config.globalResources( | |
| 'optimized-repeat' | |
| ); | |
| ViewSlot.prototype.move = function move(sourceIndex, targetIndex) { | |
| if (sourceIndex === targetIndex) return | |
| const children = this.children; | |
| const view = children[sourceIndex] | |
| view.removeNodes(); | |
| view.insertNodesBefore(children[targetIndex].firstChild); | |
| // remove | |
| children.splice(sourceIndex, 1); | |
| // readd | |
| children.splice(targetIndex, 0, view); | |
| }; | |
| } | |
| export { | |
| OptimizedRepeat | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {RepeatStrategyLocator} from 'aurelia-templating-resources/repeat-strategy-locator'; | |
| import {ArrayOptimizedRepeatStrategy} from './array-optimized-repeat-strategy'; | |
| export class OptimizedRepeatStrategyLocator extends RepeatStrategyLocator { | |
| constructor() { | |
| super(); | |
| this.matchers = []; | |
| this.strategies = []; | |
| this.addStrategy(items => items instanceof Array, new ArrayOptimizedRepeatStrategy()); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*eslint no-loop-func:0, no-unused-vars:0*/ | |
| import {inject} from 'aurelia-dependency-injection'; | |
| import {ObserverLocator} from 'aurelia-binding'; | |
| import { | |
| BoundViewFactory, | |
| TargetInstruction, | |
| ViewSlot, | |
| ViewResources, | |
| customAttribute, | |
| bindable, | |
| templateController | |
| } from 'aurelia-templating'; | |
| import {OptimizedRepeatStrategyLocator} from './optimized-repeat-strategy-locator'; | |
| import { | |
| getItemsSourceExpression, | |
| unwrapExpression, | |
| isOneTime, | |
| updateOneTimeBinding | |
| } from 'aurelia-templating-resources/repeat-utilities'; //'./repeat-utilities'; | |
| import {viewsRequireLifecycle} from 'aurelia-templating-resources/analyze-view-factory' // './analyze-view-factory'; | |
| import {AbstractRepeater} from 'aurelia-templating-resources/abstract-repeater'; //'./abstract-repeater'; | |
| /** | |
| * Binding to iterate over iterable objects (Array, Map and Number) to genereate a template for each iteration. | |
| */ | |
| @customAttribute('optimized-repeat') | |
| @templateController | |
| @inject(BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, OptimizedRepeatStrategyLocator) | |
| export class OptimizedRepeat extends AbstractRepeater { | |
| /** | |
| * List of items to bind the repeater to. | |
| * | |
| * @property items | |
| */ | |
| @bindable items | |
| /** | |
| * Local variable which gets assigned on each iteration. | |
| * | |
| * @property local | |
| */ | |
| @bindable local | |
| /** | |
| * Key when iterating over Maps. | |
| * | |
| * @property key | |
| */ | |
| @bindable key | |
| /** | |
| * Value when iterating over Maps. | |
| * | |
| * @property value | |
| */ | |
| @bindable value | |
| /** | |
| * Creates an instance of Repeat. | |
| * @param viewFactory The factory generating the view | |
| * @param instruction The instructions for how the element should be enhanced. | |
| * @param viewResources Collection of resources used to compile the the views. | |
| * @param viewSlot The slot the view is injected in to. | |
| * @param observerLocator The observer locator instance. | |
| * @param collectionStrategyLocator The strategy locator to locate best strategy to iterate the collection. | |
| */ | |
| constructor(viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator) { | |
| super({ | |
| local: 'item', | |
| viewsRequireLifecycle: viewsRequireLifecycle(viewFactory) | |
| }); | |
| this.viewFactory = viewFactory; | |
| this.instruction = instruction; | |
| this.viewSlot = viewSlot; | |
| this.lookupFunctions = viewResources.lookupFunctions; | |
| this.observerLocator = observerLocator; | |
| this.key = 'key'; | |
| this.value = 'value'; | |
| this.strategyLocator = strategyLocator; | |
| this.ignoreMutation = false; | |
| this.sourceExpression = getItemsSourceExpression(this.instruction, 'optimized-repeat.for'); | |
| this.isOneTime = isOneTime(this.sourceExpression); | |
| this.viewsRequireLifecycle = viewsRequireLifecycle(viewFactory); | |
| } | |
| call(context, changes) { | |
| this[context](this.items, changes); | |
| } | |
| /** | |
| * Binds the repeat to the binding context and override context. | |
| * @param bindingContext The binding context. | |
| * @param overrideContext An override context for binding. | |
| */ | |
| bind(bindingContext, overrideContext) { | |
| this.scope = { bindingContext, overrideContext }; | |
| this.itemsChanged(); | |
| } | |
| /** | |
| * Unbinds the repeat | |
| */ | |
| unbind() { | |
| this.scope = null; | |
| this.items = null; | |
| this.viewSlot.removeAll(true); | |
| this._unsubscribeCollection(); | |
| } | |
| _unsubscribeCollection() { | |
| if (this.collectionObserver) { | |
| this.collectionObserver.unsubscribe(this.callContext, this); | |
| this.collectionObserver = null; | |
| this.callContext = null; | |
| } | |
| } | |
| /** | |
| * Invoked everytime the item property changes. | |
| */ | |
| itemsChanged() { | |
| this._unsubscribeCollection(); | |
| // still bound? | |
| if (!this.scope) { | |
| return; | |
| } | |
| let items = this.items; | |
| this.strategy = this.strategyLocator.getStrategy(items); | |
| if (!this.strategy) { | |
| throw new Error(`Value for '${this.sourceExpression}' is non-repeatable`); | |
| } | |
| if (!this.isOneTime && !this._observeInnerCollection()) { | |
| this._observeCollection(); | |
| } | |
| this.strategy.instanceChanged(this, items); | |
| } | |
| _getInnerCollection() { | |
| let expression = unwrapExpression(this.sourceExpression); | |
| if (!expression) { | |
| return null; | |
| } | |
| return expression.evaluate(this.scope, null); | |
| } | |
| /** | |
| * Invoked when the underlying collection changes. | |
| */ | |
| handleCollectionMutated(collection, changes) { | |
| if (!this.collectionObserver) { | |
| return; | |
| } | |
| this.strategy.instanceMutated(this, collection, changes); | |
| } | |
| /** | |
| * Invoked when the underlying inner collection changes. | |
| */ | |
| handleInnerCollectionMutated(collection, changes) { | |
| if (!this.collectionObserver) { | |
| return; | |
| } | |
| // guard against source expressions that have observable side-effects that could | |
| // cause an infinite loop- eg a value converter that mutates the source array. | |
| if (this.ignoreMutation) { | |
| return; | |
| } | |
| this.ignoreMutation = true; | |
| let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); | |
| this.observerLocator.taskQueue.queueMicroTask(() => this.ignoreMutation = false); | |
| // call itemsChanged... | |
| if (newItems === this.items) { | |
| // call itemsChanged directly. | |
| this.itemsChanged(); | |
| } else { | |
| // call itemsChanged indirectly by assigning the new collection value to | |
| // the items property, which will trigger the self-subscriber to call itemsChanged. | |
| this.items = newItems; | |
| } | |
| } | |
| _observeInnerCollection() { | |
| let items = this._getInnerCollection(); | |
| let strategy = this.strategyLocator.getStrategy(items); | |
| if (!strategy) { | |
| return false; | |
| } | |
| this.collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); | |
| if (!this.collectionObserver) { | |
| return false; | |
| } | |
| this.callContext = 'handleInnerCollectionMutated'; | |
| this.collectionObserver.subscribe(this.callContext, this); | |
| return true; | |
| } | |
| _observeCollection() { | |
| let items = this.items; | |
| this.collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, items); | |
| if (this.collectionObserver) { | |
| this.callContext = 'handleCollectionMutated'; | |
| this.collectionObserver.subscribe(this.callContext, this); | |
| } | |
| } | |
| // @override AbstractRepeater | |
| viewCount() { return this.viewSlot.children.length; } | |
| views() { return this.viewSlot.children; } | |
| view(index) { return this.viewSlot.children[index]; } | |
| addView(bindingContext, overrideContext) { | |
| let view = this.viewFactory.create(); | |
| view.bind(bindingContext, overrideContext); | |
| this.viewSlot.add(view); | |
| } | |
| insertView(index, bindingContext, overrideContext) { | |
| let view = this.viewFactory.create(); | |
| view.bind(bindingContext, overrideContext); | |
| this.viewSlot.insert(index, view); | |
| } | |
| moveView(sourceIndex, targetIndex) { | |
| this.viewSlot.move(sourceIndex, targetIndex); | |
| } | |
| removeAllViews(returnToCache, skipAnimation) { | |
| return this.viewSlot.removeAll(returnToCache, skipAnimation); | |
| } | |
| removeView(index, returnToCache, skipAnimation) { | |
| return this.viewSlot.removeAt(index, returnToCache, skipAnimation); | |
| } | |
| updateBindings(view: View) { | |
| let j = view.bindings.length; | |
| while (j--) { | |
| updateOneTimeBinding(view.bindings[j]); | |
| } | |
| j = view.controllers.length; | |
| while (j--) { | |
| let k = view.controllers[j].boundProperties.length; | |
| while (k--) { | |
| let binding = view.controllers[j].boundProperties[k].binding; | |
| updateOneTimeBinding(binding); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment