diff --git a/modules/angular2/src/change_detection/dynamic_change_detector.js b/modules/angular2/src/change_detection/dynamic_change_detector.js index 9acd2eb316..d735220894 100644 --- a/modules/angular2/src/change_detection/dynamic_change_detector.js +++ b/modules/angular2/src/change_detection/dynamic_change_detector.js @@ -39,14 +39,13 @@ export class DynamicChangeDetector extends AbstractChangeDetector { this.formatters = formatters; this.values = ListWrapper.createFixedSize(protoRecords.length + 1); - ListWrapper.fill(this.values, uninitialized); - this.changes = ListWrapper.createFixedSize(protoRecords.length + 1); this.protos = protoRecords; } setContext(context:any) { + ListWrapper.fill(this.values, uninitialized); this.values[0] = context; } diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 8056f3ef60..7277e97dd6 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -15,12 +15,17 @@ import {ViewPort} from './viewport'; import {Content} from './shadow_dom_emulation/content_tag'; import {LightDom, DestinationLightDom} from './shadow_dom_emulation/light_dom'; import {ShadowDomStrategy} from './shadow_dom_strategy'; +import {ViewPool} from './view_pool'; const NG_BINDING_CLASS = 'ng-binding'; const NG_BINDING_CLASS_SELECTOR = '.ng-binding'; // TODO(tbosch): Cannot use `const` because of Dart. var NO_FORMATTERS = MapWrapper.create(); +// TODO(rado): make this configurable/smarter. +var VIEW_POOL_CAPACITY = 10000; +var VIEW_POOL_PREFILL = 0; + /** * Const of making objects: http://jsperf.com/instantiate-size-of-object */ @@ -268,6 +273,7 @@ export class ProtoView { rootBindingOffset:int; isTemplateElement:boolean; shadowDomStrategy: ShadowDomStrategy; + _viewPool: ViewPool; constructor( template:Element, protoChangeDetector:ProtoChangeDetector, @@ -284,10 +290,23 @@ export class ProtoView { ? 1 : 0; this.isTemplateElement = this.element instanceof TemplateElement; this.shadowDomStrategy = shadowDomStrategy; + this._viewPool = new ViewPool(VIEW_POOL_CAPACITY); } // TODO(rado): hostElementInjector should be moved to hydrate phase. instantiate(hostElementInjector: ElementInjector):View { + if (this._viewPool.length() == 0) this._preFillPool(hostElementInjector); + var view = this._viewPool.pop(); + return isPresent(view) ? view : this._instantiate(hostElementInjector); + } + + _preFillPool(hostElementInjector: ElementInjector) { + for (var i = 0; i < VIEW_POOL_PREFILL; i++) { + this._viewPool.push(this._instantiate(hostElementInjector)); + } + } + + _instantiate(hostElementInjector: ElementInjector): View { var rootElementClone = this.instantiateInPlace ? this.element : DOM.clone(this.element); var elementsWithBindingsDynamic; if (this.isTemplateElement) { @@ -409,6 +428,10 @@ export class ProtoView { return view; } + returnToPool(view: View) { + this._viewPool.push(view); + } + static _addNativeEventListener(element: Element, eventName: string, expr: AST, view: View) { var locals = MapWrapper.create(); var innerCallback = ProtoView.buildInnerCallback(expr, view, locals); diff --git a/modules/angular2/src/core/compiler/view_pool.js b/modules/angular2/src/core/compiler/view_pool.js new file mode 100644 index 0000000000..8c9b5bce44 --- /dev/null +++ b/modules/angular2/src/core/compiler/view_pool.js @@ -0,0 +1,26 @@ +import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'angular2/src/facade/collection'; +import {View} from './view'; + +export class ViewPool { + _views: List; + _capacity: number; + constructor(capacity: number) { + this._views = []; + this._capacity = capacity; + } + + pop(): View { + return ListWrapper.isEmpty(this._views) ? null : ListWrapper.removeLast(this._views); + } + + push(view: View) { + if (this._views.length < this._capacity) { + ListWrapper.push(this._views, view); + } + } + + length() { + return this._views.length; + } +} + diff --git a/modules/angular2/src/core/compiler/viewport.js b/modules/angular2/src/core/compiler/viewport.js index 3e4bdadd30..bc2cd31e0b 100644 --- a/modules/angular2/src/core/compiler/viewport.js +++ b/modules/angular2/src/core/compiler/viewport.js @@ -92,6 +92,8 @@ export class ViewPort { if (atIndex == -1) atIndex = this._views.length - 1; var view = this.detach(atIndex); view.dehydrate(); + // TODO(rado): this needs to be delayed until after any pending animations. + this.defaultProtoView.returnToPool(view); // view is intentionally not returned to the client. } diff --git a/modules/angular2/src/facade/collection.dart b/modules/angular2/src/facade/collection.dart index 2d9e111101..6fd090ae7f 100644 --- a/modules/angular2/src/facade/collection.dart +++ b/modules/angular2/src/facade/collection.dart @@ -110,6 +110,7 @@ class ListWrapper { list.remove(items[i]); } } + static removeLast(List list) => list.removeLast(); static bool remove(List list, item) => list.remove(item); static void clear(List l) { l.clear(); diff --git a/modules/angular2/src/facade/collection.es6 b/modules/angular2/src/facade/collection.es6 index b1dd79c59a..ef1f885703 100644 --- a/modules/angular2/src/facade/collection.es6 +++ b/modules/angular2/src/facade/collection.es6 @@ -149,6 +149,9 @@ export class ListWrapper { list.splice(index, 1); } } + static removeLast(list:List) { + return list.pop(); + } static remove(list, el): boolean { var index = list.indexOf(el); if (index > -1) { diff --git a/modules/angular2/test/core/compiler/view_pool_spec.js b/modules/angular2/test/core/compiler/view_pool_spec.js new file mode 100644 index 0000000000..e1e7065259 --- /dev/null +++ b/modules/angular2/test/core/compiler/view_pool_spec.js @@ -0,0 +1,46 @@ +import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'angular2/test_lib'; + +import {View} from 'angular2/src/core/compiler/view'; +import {ViewPool} from 'angular2/src/core/compiler/view_pool'; +import {proxy, IMPLEMENTS} from 'angular2/src/facade/lang'; + +@proxy +@IMPLEMENTS(View) +class FakeView { + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + +export function main() { + describe('ViewPool', () => { + var viewPool, capacity = 3; + beforeEach(() => { + viewPool = new ViewPool(capacity); + }) + + it('should return null when there are no views', () => { + expect(viewPool.pop()).toBeNull(); + expect(viewPool.length()).toBe(0); + }) + + it('should support storing and retrieving a view', () => { + var view = new FakeView(); + viewPool.push(view); + expect(viewPool.length()).toBe(1); + + expect(viewPool.pop()).toBe(view); + expect(viewPool.length()).toBe(0); + }) + + it('should not store more views that its capacity', () => { + for (var i = 0; i < capacity * 2; i++) viewPool.push(new FakeView()); + expect(viewPool.length()).toBe(capacity); + + for (var i = 0; i < capacity; i++) { + expect(viewPool.pop()).not.toBe(null); + } + expect(viewPool.pop()).toBeNull(); + }) + }) +} diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js index 970f9ee3d7..7b49c1d862 100644 --- a/modules/angular2/test/core/compiler/view_spec.js +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -31,6 +31,13 @@ class FakeViewPort { } } +@proxy +@IMPLEMENTS(View) +class FakeView { + noSuchMethod(i) { + super.noSuchMethod(i); + } +} export function main() { describe('view', function() { @@ -68,6 +75,14 @@ export function main() { view.dehydrate(); expect(view.hydrated()).toBe(false); }); + + it('should use the view pool to reuse views', () => { + var pv = new ProtoView(el('
'), new DynamicProtoChangeDetector(), null); + var fakeView = new FakeView(); + pv.returnToPool(fakeView); + + expect(pv.instantiate(null)).toBe(fakeView); + }); }); describe('with locals', function() { diff --git a/modules/benchmarks/src/tree/tree_benchmark.js b/modules/benchmarks/src/tree/tree_benchmark.js index 80cdbc3289..73e8254bc3 100644 --- a/modules/benchmarks/src/tree/tree_benchmark.js +++ b/modules/benchmarks/src/tree/tree_benchmark.js @@ -43,7 +43,7 @@ function setupReflector() { }, template: new TemplateConfig({ directives: [TreeComponent, NgIf], - inline: `{{data.value}}` + inline: ` {{data.value}} ` }) })] });