Skip to content

Instantly share code, notes, and snippets.

@niieani
Last active April 23, 2016 23:36
Show Gist options
  • Save niieani/2c6423601fbecde7ef62b4087564576d to your computer and use it in GitHub Desktop.
Save niieani/2c6423601fbecde7ef62b4087564576d to your computer and use it in GitHub Desktop.
Aurelia: Suboptimal repeat element lifecycle [swap-arrays/reverse + optimized-repeat]
<template>
<require from="./component"></require>
<button click.delegate="swapArrays()">Swap arrays</button>
<button click.delegate="reverse()">Reverse</button>
<br>
<component
optimized-repeat.for="id of components"
text.bind="id">
[${$index}]
</component>
</template>
import {Component} from './component'
export class App {
constructor() {
this.components = []
for (let i = 1; i <= 10; i++) {
this.components.push(i)
}
this.componentsAlternative = this.components.slice(0).reverse()
}
swapArrays() {
[this.components, this.componentsAlternative] = [this.componentsAlternative, this.components]
}
reverse() {
this.components.reverse()
}
}
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';
/**
* Returns the index of the element in an array, optionally using a matcher function.
*/
export function indexOf(array, item, matcher, startIndex) {
if (!matcher) {
// native indexOf is more performant than a for loop
return array.indexOf(item);
}
const length = array.length;
for (let index = startIndex || 0; index <= length; index++) {
const testItem = array[index];
if (matcher(testItem, item)) {
return index;
}
}
return -1;
}
/**
* 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) {
const itemsLength = items.length;
// if the new instance does not contain any items,
// just remove all views and don't do any further processing
if (!items || itemsLength === 0) {
repeat.removeAllViews(true);
return;
}
const children = repeat.views();
const viewsLength = children.length;
// likewise, if we previously didn't have any views,
// simply make them and return
if (viewsLength === 0) {
this._standardProcessInstanceChanged(repeat, items);
return;
}
if (repeat.viewsRequireLifecycle) {
const childrenSnapshot = children.slice(0);
const itemNameInBindingContext = repeat.local;
const matcher = repeat.matcher();
// the cache of the current state (it will be transformed along with the views to keep track of indicies)
let itemsPreviouslyInViews = [];
const viewsToRemove = [];
for (let index = 0; index < viewsLength; index++) {
const view = childrenSnapshot[index];
const oldItem = view.bindingContext[itemNameInBindingContext];
if (indexOf(items, oldItem, matcher) === -1) {
// remove the item if no longer in the new instance of items
viewsToRemove.push(view);
} else {
// or add the item to the cache list
itemsPreviouslyInViews.push(oldItem);
}
}
let updateViews;
let removePromise;
if (itemsPreviouslyInViews.length > 0) {
removePromise = repeat.removeViews(viewsToRemove, true);
updateViews = () => {
// update views (create new and move existing)
for (let index = 0; index < itemsLength; index++) {
const item = items[index];
const indexOfView = indexOf(itemsPreviouslyInViews, item, matcher, index);
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);
}
}
// remove extraneous elements in case of duplicates,
// also update binding contexts if objects changed using the matcher function
this._inPlaceProcessItems(repeat, items);
}
} else {
// if all of the items are different, remove all and add all from scratch
removePromise = repeat.removeAllViews(true);
updateViews = () => this._standardProcessInstanceChanged(repeat, items);
}
if (removePromise instanceof Promise) {
removePromise.then(updateViews);
} else {
updateViews();
}
} else {
// no lifecycle needed, use the fast in-place processing
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;
}
}
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)
}
}
<!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>
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));
});
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);
};
ViewSlot.prototype.removeMany = function removeMany(viewsToRemove: View[], returnToCache?: boolean, skipAnimation?: boolean): void | Promise<View> {
const children = this.children;
let ii = viewsToRemove.length;
let i;
let rmPromises = [];
viewsToRemove.forEach(child => {
if (skipAnimation) {
child.removeNodes();
return;
}
let animatableElement = getAnimatableElement(child);
if (animatableElement !== null) {
rmPromises.push(this.animator.leave(animatableElement).then(() => child.removeNodes()));
} else {
child.removeNodes();
}
});
let removeAction = () => {
if (this.isAttached) {
for (i = 0; i < ii; ++i) {
viewsToRemove[i].detached();
}
}
if (returnToCache) {
for (i = 0; i < ii; ++i) {
viewsToRemove[i].returnToCache();
}
}
for (i = 0; i < ii; ++i) {
const index = children.indexOf(viewsToRemove[i]);
if (index >= 0) {
children.splice(index, 1);
}
}
};
if (rmPromises.length > 0) {
return Promise.all(rmPromises).then(() => removeAction());
}
removeAction();
}
}
export {
OptimizedRepeat
};
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());
}
}
/*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]; }
matcher() { return (this.scope.bindingContext ? this.scope.bindingContext.matcher : null) || null; }
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);
}
removeViews(viewsToRemove, returnToCache, skipAnimation) {
return this.viewSlot.removeMany(viewsToRemove, 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