diff --git a/modules/change_detection/src/change_detection_jit_generator.es6 b/modules/change_detection/src/change_detection_jit_generator.es6 index 074dbac96a..dc9282c185 100644 --- a/modules/change_detection/src/change_detection_jit_generator.es6 +++ b/modules/change_detection/src/change_detection_jit_generator.es6 @@ -296,7 +296,7 @@ export class ChangeDetectorJITGenerator { switch (r.mode) { case RECORD_TYPE_SELF: - throw new BaseException("Cannot evaluate self"); + return assignmentTemplate(newValue, context); case RECORD_TYPE_CONST: return `${newValue} = ${this.genLiteral(r.funcOrValue)}`; diff --git a/modules/change_detection/src/coalesce.js b/modules/change_detection/src/coalesce.js new file mode 100644 index 0000000000..2b2f087e37 --- /dev/null +++ b/modules/change_detection/src/coalesce.js @@ -0,0 +1,88 @@ +import {isPresent} from 'facade/lang'; +import {List, ListWrapper, Map, MapWrapper} from 'facade/collection'; +import {RECORD_TYPE_SELF, ProtoRecord} from './proto_change_detector'; + +/** + * Removes "duplicate" records. It assuming that record evaluation does not + * have side-effects. + * + * Records that are not last in bindings are removed and all the indices + * of the records that depend on them are updated. + * + * Records that are last in bindings CANNOT be removed, and instead are + * replaced with very cheap SELF records. + */ +export function coalesce(records:List):List { + var res = ListWrapper.create(); + var indexMap = MapWrapper.create(); + + for (var i = 0; i < records.length; ++i) { + var r = records[i]; + var record = _replaceIndices(r, res.length + 1, indexMap); + var matchingRecord = _findMatching(record, res); + + if (isPresent(matchingRecord) && record.lastInBinding) { + ListWrapper.push(res, _selfRecord(record, matchingRecord.selfIndex, res.length + 1)); + MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex); + + } else if (isPresent(matchingRecord) && !record.lastInBinding) { + MapWrapper.set(indexMap, r.selfIndex, matchingRecord.selfIndex); + + } else { + ListWrapper.push(res, record); + MapWrapper.set(indexMap, r.selfIndex, record.selfIndex); + } + } + + return res; +} + +function _selfRecord(r:ProtoRecord, contextIndex:number, selfIndex:number):ProtoRecord { + return new ProtoRecord( + RECORD_TYPE_SELF, + "self", + null, + [], + r.fixedArgs, + contextIndex, + selfIndex, + r.bindingMemento, + r.groupMemento, + r.expressionAsString, + r.lastInBinding, + r.lastInGroup + ); +} + +function _findMatching(r:ProtoRecord, rs:List){ + return ListWrapper.find(rs, (rr) => + rr.mode === r.mode && + rr.funcOrValue === r.funcOrValue && + rr.contextIndex === r.contextIndex && + ListWrapper.equals(rr.args, r.args) + ); +} + +function _replaceIndices(r:ProtoRecord, selfIndex:number, indexMap:Map) { + var args = ListWrapper.map(r.args, (a) => _map(indexMap, a)); + var contextIndex = _map(indexMap, r.contextIndex); + return new ProtoRecord( + r.mode, + r.name, + r.funcOrValue, + args, + r.fixedArgs, + contextIndex, + selfIndex, + r.bindingMemento, + r.groupMemento, + r.expressionAsString, + r.lastInBinding, + r.lastInGroup + ); +} + +function _map(indexMap:Map, value:number) { + var r = MapWrapper.get(indexMap, value) + return isPresent(r) ? r : value; +} \ No newline at end of file diff --git a/modules/change_detection/src/dynamic_change_detector.js b/modules/change_detection/src/dynamic_change_detector.js index 2484e92b1d..8bbbf41421 100644 --- a/modules/change_detection/src/dynamic_change_detector.js +++ b/modules/change_detection/src/dynamic_change_detector.js @@ -100,7 +100,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector { _calculateCurrValue(proto:ProtoRecord) { switch (proto.mode) { case RECORD_TYPE_SELF: - throw new BaseException("Cannot evaluate self"); + return this._readContext(proto); case RECORD_TYPE_CONST: return proto.funcOrValue; diff --git a/modules/change_detection/src/proto_change_detector.js b/modules/change_detection/src/proto_change_detector.js index 12e5b74cbf..0c139627d6 100644 --- a/modules/change_detection/src/proto_change_detector.js +++ b/modules/change_detection/src/proto_change_detector.js @@ -31,6 +31,7 @@ import {ChangeDetectorJITGenerator} from './change_detection_jit_generator'; import {ArrayChanges} from './array_changes'; import {KeyValueChanges} from './keyvalue_changes'; +import {coalesce} from './coalesce'; export const RECORD_TYPE_SELF = 0; export const RECORD_TYPE_CONST = 1; @@ -66,7 +67,9 @@ export class ProtoRecord { selfIndex:number, bindingMemento:any, groupMemento:any, - expressionAsString:string) { + expressionAsString:string, + lastInBinding:boolean, + lastInGroup:boolean) { this.mode = mode; this.name = name; @@ -77,8 +80,8 @@ export class ProtoRecord { this.selfIndex = selfIndex; this.bindingMemento = bindingMemento; this.groupMemento = groupMemento; - this.lastInBinding = false; - this.lastInGroup = false; + this.lastInBinding = lastInBinding; + this.lastInGroup = lastInGroup; this.expressionAsString = expressionAsString; } } @@ -91,9 +94,11 @@ export class ProtoChangeDetector { } export class DynamicProtoChangeDetector extends ProtoChangeDetector { + _records:List; _recordBuilder:ProtoRecordBuilder; constructor() { + this._records = null; this._recordBuilder = new ProtoRecordBuilder(); } @@ -102,8 +107,15 @@ export class DynamicProtoChangeDetector extends ProtoChangeDetector { } instantiate(dispatcher:any, formatters:Map) { - var records = this._recordBuilder.records; - return new DynamicChangeDetector(dispatcher, formatters, records); + this._createRecordsIfNecessary(); + return new DynamicChangeDetector(dispatcher, formatters, this._records); + } + + _createRecordsIfNecessary() { + if (isBlank(this._records)) { + var records = this._recordBuilder.records; + this._records = coalesce(records); + } } } @@ -113,6 +125,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector { _recordBuilder:ProtoRecordBuilder; constructor() { + this._factory = null; this._recordBuilder = new ProtoRecordBuilder(); } @@ -128,7 +141,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector { _createFactoryIfNecessary() { if (isBlank(this._factory)) { var c = _jitProtoChangeDetectorClassCounter++; - var records = this._recordBuilder.records; + var records = coalesce(this._recordBuilder.records); var typeName = `ChangeDetector${c}`; this._factory = new ChangeDetectorJITGenerator(typeName, records).generate(); } @@ -273,7 +286,7 @@ class _ConvertAstIntoProtoRecords { var selfIndex = ++ this.contextIndex; ListWrapper.push(this.protoRecords, new ProtoRecord(type, name, funcOrValue, args, fixedArgs, context, selfIndex, - this.bindingMemento, this.groupMemento, this.expressionAsString)); + this.bindingMemento, this.groupMemento, this.expressionAsString, false, false)); return selfIndex; } } diff --git a/modules/change_detection/test/coalesce_spec.js b/modules/change_detection/test/coalesce_spec.js new file mode 100644 index 0000000000..fcddcadcff --- /dev/null +++ b/modules/change_detection/test/coalesce_spec.js @@ -0,0 +1,82 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; + +import {coalesce} from 'change_detection/coalesce'; +import {RECORD_TYPE_SELF, ProtoRecord} from 'change_detection/proto_change_detector'; + +export function main() { + function r(funcOrValue, args, contextIndex, selfIndex, lastInBinding = false) { + return new ProtoRecord(99, "name", funcOrValue, args, null, contextIndex, selfIndex, + null, null, null, lastInBinding, false); + } + + describe("change detection - coalesce", () => { + it("should work with an empty list", () => { + expect(coalesce([])).toEqual([]); + }); + + it("should remove non-terminal duplicate records" + + " and update the context indices referencing them", () => { + var rs = coalesce([ + r("user", [], 0, 1), + r("first", [], 1, 2), + r("user", [], 0, 3), + r("last", [], 3, 4) + ]); + + expect(rs).toEqual([ + r("user", [], 0, 1), + r("first", [], 1, 2), + r("last", [], 1, 3) + ]); + }); + + it("should update indices of other records", () => { + var rs = coalesce([ + r("dup", [], 0, 1), + r("dup", [], 0, 2), + r("user", [], 0, 3), + r("first", [3], 3, 4) + ]); + + expect(rs).toEqual([ + r("dup", [], 0, 1), + r("user", [], 0, 2), + r("first", [2], 2, 3) + ]); + }); + + it("should remove non-terminal duplicate records" + + " and update the args indices referencing them", () => { + var rs = coalesce([ + r("user1", [], 0, 1), + r("user2", [], 0, 2), + r("hi", [1], 0, 3), + r("hi", [1], 0, 4), + r("hi", [2], 0, 5) + ]); + + expect(rs).toEqual([ + r("user1", [], 0, 1), + r("user2", [], 0, 2), + r("hi", [1], 0, 3), + r("hi", [2], 0, 4) + ]); + }); + + it("should replace duplicate terminal records with" + + " self records", () => { + + var rs = coalesce([ + r("user", [], 0, 1, true), + r("user", [], 0, 2, true) + ]); + + expect(rs[1]).toEqual(new ProtoRecord( + RECORD_TYPE_SELF, "self", null, + [], null, 1, 2, + null, null, null, + true, false) + ); + }); + }); +} diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index e3da87e3d7..ef79726791 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -118,6 +118,13 @@ class ListWrapper { } l.fillRange(start, end, value); } + static bool equals(List a, List b){ + if(a.length != b.length) return false; + for (var i = 0; i < a.length; ++i) { + if (a[i] != b[i]) return false; + } + return true; + } } bool isListLikeIterable(obj) => obj is Iterable; diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index e5804f6e50..0a8d5cd9e5 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -169,6 +169,13 @@ export class ListWrapper { static fill(list:List, value, start:int = 0, end:int = undefined) { list.fill(value, start, end); } + static equals(a:List, b:List):boolean { + if(a.length != b.length) return false; + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; + } } export function isListLikeIterable(obj):boolean {