Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Optimize ngRepeat by using the new CD #751

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions lib/core/parser/syntax.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ abstract class Visitor {
visitPrefix(Prefix expression) => visitExpression(expression);

visitLiteral(Literal expression) => visitExpression(expression);
visitLiteralPrimitive(LiteralPrimitive expression)
=> visitLiteral(expression);
visitLiteralPrimitive(LiteralPrimitive expression) =>
visitLiteral(expression);
visitLiteralString(LiteralString expression) => visitLiteral(expression);
visitLiteralArray(LiteralArray expression) => visitLiteral(expression);
visitLiteralObject(LiteralObject expression) => visitLiteral(expression);
Expand All @@ -39,12 +39,12 @@ abstract class Expression {
bool get isAssignable => false;
bool get isChain => false;

eval(scope, [FilterMap filters = defaultFilterMap])
=> throw new EvalError("Cannot evaluate $this");
assign(scope, value)
=> throw new EvalError("Cannot assign to $this");
bind(context, [LocalsWrapper wrapper])
=> new BoundExpression(this, context, wrapper);
eval(scope, [FilterMap filters = defaultFilterMap]) =>
throw new EvalError("Cannot evaluate $this");
assign(scope, value) =>
throw new EvalError("Cannot assign to $this");
bind(context, [LocalsWrapper wrapper]) =>
new BoundExpression(this, context, wrapper);

accept(Visitor visitor);
String toString() => Unparser.unparse(this);
Expand Down
8 changes: 4 additions & 4 deletions lib/core_dom/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class View {
class ViewPort {
final dom.Node placeholder;
final NgAnimate _animate;
final List<View> _views = <View>[];
final _views = <View>[];

ViewPort(this.placeholder, this._animate);

Expand All @@ -56,12 +56,12 @@ class ViewPort {
}

void _viewsInsertAfter(View view, View insertAfter) {
int index = (insertAfter != null) ? _views.indexOf(insertAfter) : -1;
_views.insert(index + 1, view);
int index = insertAfter == null ? 0 : _views.indexOf(insertAfter) + 1;
_views.insert(index, view);
}

dom.Node _lastNode(View insertAfter) =>
insertAfter == null
? placeholder
: insertAfter.nodes[insertAfter.nodes.length - 1];
: insertAfter.nodes.last;
}
265 changes: 133 additions & 132 deletions lib/directive/ng_repeat.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
part of angular.directive;

class _Row {
var id;
Scope scope;
View view;
dom.Element startNode;
dom.Element endNode;
List<dom.Element> nodes;

_Row(this.id);
}

/**
* The `ngRepeat` directive instantiates a template once per item from a
* collection. Each template instance gets its own scope, where the given loop
Expand All @@ -20,15 +9,15 @@ class _Row {
* Special properties are exposed on the local scope of each template instance,
* including:
*
* <table>
* <tr><th> Variable </th><th> Type </th><th> Details <th></tr>
* <tr><td> `$index` </td><td>[num] </td><td> iterator offset of the repeated element (0..length-1) <td></tr>
* <tr><td> `$first` </td><td>[bool]</td><td> true if the repeated element is first in the iterator. <td></tr>
* <tr><td> `$middle` </td><td>[bool]</td><td> true if the repeated element is between the first and last in the iterator. <td></tr>
* <tr><td> `$last` </td><td>[bool]</td><td> true if the repeated element is last in the iterator. <td></tr>
* <tr><td> `$even` </td><td>[bool]</td><td> true if the iterator position `$index` is even (otherwise false). <td></tr>
* <tr><td> `$odd` </td><td>[bool]</td><td> true if the iterator position `$index` is odd (otherwise false). <td></tr>
* </table>
* * `$index` ([:num:]) the iterator offset of the repeated element
* (0..length-1)
* * `$first` ([:bool:]) whether the repeated element is first in the
* iterator.
* * `$middle` ([:bool:]) whether the repeated element is between the first
* and last in the iterator.
* * `$last` ([:bool:]) whether the repeated element is last in the iterator.
* * `$even` ([:bool:]) whether the iterator position `$index` is even.
* * `$odd` ([:bool:]) whether the iterator position `$index` is odd.
*
*
* [repeat_expression] ngRepeat The expression indicating how to enumerate a
Expand Down Expand Up @@ -57,7 +46,7 @@ class _Row {
* function can be used to assign a unique `$$hashKey` property to each item
* in the array. This property is then used as a key to associated DOM
* elements with the corresponding item in the array by identity. Moving the
* same object in array would move the DOM element in the same way ian the
* same object in array would move the DOM element in the same way in the
* DOM.
*
* For example: `item in items track by item.id` is a typical pattern when
Expand All @@ -81,8 +70,8 @@ class _Row {
selector: '[ng-repeat]',
map: const {'.': '@expression'})
class NgRepeatDirective {
static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?(\s+lazily\s*)?$');
static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$');
static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(?:track\s+by\s+(.+)\s*)?(\s+lazily\s*)?$');
static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([$\w]+)|\(([$\w]+)\s*,\s*([$\w]+)\))$');

final ViewPort _viewPort;
final BoundViewFactory _boundViewFactory;
Expand All @@ -95,156 +84,168 @@ class NgRepeatDirective {
String _valueIdentifier;
String _keyIdentifier;
String _listExpr;
Map<dynamic, _Row> _rows = {};
Function _trackByIdFn = (key, value, index) => value;
Watch _watch = null;
Iterable _lastCollection;
List<_Row> _rows;
Function _generateId = (key, value, index) => value;
Watch _watch;

NgRepeatDirective(this._viewPort, this._boundViewFactory,
this._scope, this._parser, this._astParser,
this.filters);
NgRepeatDirective(this._viewPort, this._boundViewFactory, this._scope,
this._parser, this._astParser, this.filters);

set expression(value) {
assert(value != null);
_expression = value;
if (_watch != null) _watch.remove();

Match match = _SYNTAX.firstMatch(_expression);
if (match == null) {
throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ "
"in _collection_[ track by _id_]' but got '$_expression'.";
}

_listExpr = match.group(2);
var trackByExpr = match.group(4);

var trackByExpr = match.group(3);
if (trackByExpr != null) {
Expression trackBy = _parser(trackByExpr);
_trackByIdFn = ((key, value, index) {
final trackByLocals = <String, Object>{};
if (_keyIdentifier != null) trackByLocals[_keyIdentifier] = key;
trackByLocals
_generateId = ((key, value, index) {
final context = <String, Object>{}
..[_valueIdentifier] = value
..[r'$index'] = index
..[r'$id'] = (obj) => obj;
return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, trackByLocals));
if (_keyIdentifier != null) context[_keyIdentifier] = key;
return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context,
context));
});
}

var assignExpr = match.group(1);
match = _LHS_SYNTAX.firstMatch(assignExpr);
if (match == null) {
throw "[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' "
"should be an identifier or '(_key_, _value_)' expression, but got "
"'$assignExpr'.";
}

_valueIdentifier = match.group(3);
if (_valueIdentifier == null) _valueIdentifier = match.group(1);
_keyIdentifier = match.group(2);

_watch = _scope.watch(
_astParser(_listExpr, collection: true, filters: filters),
(CollectionChangeRecord collection, _) {
//TODO(misko): we should take advantage of the CollectionChangeRecord!
_onCollectionChange(collection == null ? [] : collection.iterable);
(CollectionChangeRecord changes, _) {
if (changes is! CollectionChangeRecord) return;
_onChange(changes);
}
);
}

List<_Row> _computeNewRows(Iterable collection, trackById) {
final newRowOrder = new List<_Row>(collection.length);
// Same as lastViewMap but it has the current state. It will become the
// lastViewMap on the next iteration.
final newRows = <dynamic, _Row>{};
// locate existing items
for (var index = 0; index < newRowOrder.length; index++) {
var value = collection.elementAt(index);
trackById = _trackByIdFn(index, value, index);
if (_rows.containsKey(trackById)) {
var row = _rows[trackById];
_rows.remove(trackById);
newRows[trackById] = row;
newRowOrder[index] = row;
} else if (newRows.containsKey(trackById)) {
// restore lastViewMap
newRowOrder.forEach((row) {
if (row != null && row.startNode != null) _rows[row.id] = row;
});
// This is a duplicate and we need to throw an error
throw "[NgErr50] ngRepeat error! Duplicates in a repeater are not "
"allowed. Use 'track by' expression to specify unique keys. "
"Repeater: $_expression, Duplicate key: $trackById";
} else {
// new never before seen row
newRowOrder[index] = new _Row(trackById);
newRows[trackById] = null;
// Computes and executes DOM changes when the item list changes
void _onChange(CollectionChangeRecord changes) {
final int length = changes.iterable.length;
final rows = new List<_Row>(length);
final changeFunctions = new List<Function>(length);
final removedIndexes = <int>[];
final int domLength = _rows == null ? 0 : _rows.length;
final leftInDom = new List.generate(domLength, (i) => domLength - 1 - i);
var domIndex;

var addRow = (int index, value, View previousView) {
var childContext = _updateContext(new PrototypeMap(_scope.context), index,
length)..[_valueIdentifier] = value;
var childScope = _scope.createChild(childContext);
var view = _boundViewFactory(childScope);
var nodes = view.nodes;
rows[index] = new _Row(_generateId(index, value, index))
..view = view
..scope = childScope
..nodes = nodes
..startNode = nodes.first
..endNode = nodes.last;
_viewPort.insert(view, insertAfter: previousView);
};

if (_rows == null) {
_rows = new List<_Row>(length);
for (var i = 0; i < length; i++) {
changeFunctions[i] = (index, previousView) {
addRow(index, changes.iterable.elementAt(i), previousView);
};
}
} else {
changes.forEachRemoval((removal) {
var index = removal.previousIndex;
var row = _rows[index];
row.scope.destroy();
_viewPort.remove(row.view);
leftInDom.removeAt(domLength - 1 - index);
});

changes.forEachAddition((addition) {
changeFunctions[addition.currentIndex] = (index, previousView) {
addRow(index, addition.item, previousView);
};
});

changes.forEachMove((move) {
var previousIndex = move.previousIndex;
var value = move.item;
changeFunctions[move.currentIndex] = (index, previousView) {
var previousRow = _rows[previousIndex];
var childScope = previousRow.scope;
var childContext = _updateContext(childScope.context, index, length);
if (!identical(childScope.context[_valueIdentifier], value)) {
childContext[_valueIdentifier] = value;
}
rows[index] = _rows[previousIndex];
// Only move the DOM node when required
if (domIndex < 0 || leftInDom[domIndex] != previousIndex) {
_viewPort.move(previousRow.view, moveAfter: previousView);
leftInDom.remove(previousIndex);
}
domIndex--;
};
});
}
// remove existing items
_rows.forEach((key, row) {
_viewPort.remove(row.view);
row.scope.destroy();
});
_rows = newRows;
return newRowOrder;
}

_onCollectionChange(Iterable collection) {
dom.Node previousNode = _viewPort.placeholder; // current position of the
// node
dom.Node nextNode;
Scope childScope;
Map childContext;
Scope trackById;
View cursor;

List<_Row> newRowOrder = _computeNewRows(collection, trackById);

for (var index = 0; index < collection.length; index++) {
var value = collection.elementAt(index);
_Row row = newRowOrder[index];

if (row.startNode != null) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = row.scope;
childContext = childScope.context as Map;

nextNode = previousNode;
do {
nextNode = nextNode.nextNode;
} while (nextNode != null);

if (row.startNode != nextNode) {
// existing item which got moved
_viewPort.move(row.view, moveAfter: cursor);
}
previousNode = row.endNode;
var previousView = null;
domIndex = leftInDom.length - 1;
for(var targetIndex = 0; targetIndex < length; targetIndex++) {
var changeFn = changeFunctions[targetIndex];
if (changeFn == null) {
rows[targetIndex] = _rows[targetIndex];
domIndex--;
// The element has not moved but `$last` and `$middle` might still need
// to be updated
_updateContext(rows[targetIndex].scope.context, targetIndex, length);
} else {
// new item which we don't know about
childScope = _scope.createChild(childContext = new PrototypeMap(_scope.context));
}

if (!identical(childScope.context[_valueIdentifier], value)) {
childContext[_valueIdentifier] = value;
changeFn(targetIndex, previousView);
}
var first = (index == 0);
var last = (index == collection.length - 1);
childContext
..[r'$index'] = index
..[r'$first'] = first
..[r'$last'] = last
..[r'$middle'] = !first && !last
..[r'$odd'] = index & 1 == 1
..[r'$even'] = index & 1 == 0;

if (row.startNode == null) {
var view = _boundViewFactory(childScope);
_rows[row.id] = row
..view = view
..scope = childScope
..nodes = view.nodes
..startNode = row.nodes[0]
..endNode = row.nodes[row.nodes.length - 1];
_viewPort.insert(view, insertAfter: cursor);
}
cursor = row.view;
previousView = rows[targetIndex].view;
}

_rows = rows;
}

PrototypeMap _updateContext(PrototypeMap context, int index, int length) {
var first = (index == 0);
var last = (index == length - 1);
return context
..[r'$index'] = index
..[r'$first'] = first
..[r'$last'] = last
..[r'$middle'] = !(first || last)
..[r'$odd'] = index.isOdd
..[r'$even'] = index.isEven;
}
}

class _Row {
final id;
Scope scope;
View view;
dom.Element startNode;
dom.Element endNode;
List<dom.Element> nodes;

_Row(this.id);
}
Loading