From e3548b497f52d98a66753a2a30f033a714b2942a Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 14 Oct 2014 16:00:35 -0400 Subject: [PATCH] feat(ElementInjector): implement ElementInjector --- gulpfile.js | 56 ++- modules/benchmarks/pubspec.yaml | 10 + modules/benchmarks/src/di/benchmark.dart | 11 + .../src/di/injector_instantiate_benchmark.js | 2 - .../src/element_injector/benchmark.dart | 11 + .../src/element_injector/benchmark.es5 | 7 + .../src/element_injector/bp.conf.es5 | 11 + .../element_injector/instantiate_benchmark.js | 33 ++ .../instantiate_directive_benchmark.js | 35 ++ .../benchmarks/src/element_injector/main.html | 0 .../src/element_injector/register_system.es5 | 10 + modules/core/src/annotations/visibility.js | 22 ++ modules/core/src/compiler/element_injector.js | 318 ++++++++++++++++-- .../test/compiler/element_injector_spec.js | 177 ++++++++++ modules/core/test/compiler/view_spec.js | 6 +- modules/di/src/annotations.js | 57 ++++ modules/di/src/binding.js | 11 +- modules/di/src/di.js | 1 + modules/di/src/injector.js | 5 +- modules/di/src/reflector.dart | 52 ++- modules/di/src/reflector.es6 | 50 ++- modules/di/test/di/reflector_spec.js | 23 ++ modules/facade/src/collection.dart | 2 + modules/facade/src/collection.es6 | 12 + modules/facade/src/math.dart | 9 + modules/facade/src/math.es6 | 1 + package.json | 1 + 27 files changed, 865 insertions(+), 68 deletions(-) create mode 100644 modules/benchmarks/pubspec.yaml create mode 100644 modules/benchmarks/src/di/benchmark.dart create mode 100644 modules/benchmarks/src/element_injector/benchmark.dart create mode 100644 modules/benchmarks/src/element_injector/benchmark.es5 create mode 100644 modules/benchmarks/src/element_injector/bp.conf.es5 create mode 100644 modules/benchmarks/src/element_injector/instantiate_benchmark.js create mode 100644 modules/benchmarks/src/element_injector/instantiate_directive_benchmark.js create mode 100644 modules/benchmarks/src/element_injector/main.html create mode 100644 modules/benchmarks/src/element_injector/register_system.es5 create mode 100644 modules/core/src/annotations/visibility.js create mode 100644 modules/core/test/compiler/element_injector_spec.js create mode 100644 modules/di/test/di/reflector_spec.js create mode 100644 modules/facade/src/math.dart create mode 100644 modules/facade/src/math.es6 diff --git a/gulpfile.js b/gulpfile.js index 399088f330..cb0a9b2de3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,7 +1,9 @@ +var shell = require('gulp-shell'); var gulp = require('gulp'); var rename = require('gulp-rename'); var watch = require('gulp-watch'); var mergeStreams = require('event-stream').merge; +var es = require('event-stream'); var connect = require('gulp-connect'); var clean = require('gulp-rimraf'); var runSequence = require('run-sequence'); @@ -225,22 +227,58 @@ gulp.task('analyze/dartanalyzer', function(done) { // ------------------ -// BENCHMARKS +// BENCHMARKS JS -var benchmarksBuildPath = 'build/benchpress'; -var benchmarksCompiledJsPath = 'build/js/benchmarks/lib'; - -gulp.task('benchmarks/build.benchpress', function () { +gulp.task('benchmarks/build.benchpress.js', function () { benchpress.build({ - benchmarksPath: benchmarksCompiledJsPath, - buildPath: benchmarksBuildPath + benchmarksPath: 'build/js/benchmarks/lib', + buildPath: 'build/benchpress/js' }) }); -gulp.task('benchmarks/build', function() { +gulp.task('benchmarks/build.js', function() { runSequence( ['jsRuntime/build', 'modules/build.prod.js'], - 'benchmarks/build.benchpress' + 'benchmarks/build.benchpress.js' + ); +}); + + +// ------------------ +// BENCHMARKS DART + +gulp.task('benchmarks/build.dart2js.dart', function () { + return gulp.src([ + "build/dart/benchmarks/lib/**/benchmark.dart" + ]).pipe(shell(['dart2js --package-root="build/dart/benchmarks/packages" -o "<%= file.path %>.js" <%= file.path %>'])); +}); + +gulp.task('benchmarks/create-bpconf.dart', function () { + var bpConfContent = "module.exports = function(c) {c.set({scripts: [{src: 'benchmark.dart.js'}]});}"; + var createBpConfJs = es.map(function(file, cb) { + var dir = path.dirname(file.path); + fs.writeFileSync(path.join(dir, "bp.conf.js"), bpConfContent); + cb(); + }); + + return gulp.src([ + "build/dart/benchmarks/lib/**/benchmark.dart" + ]).pipe(createBpConfJs); +}); + +gulp.task('benchmarks/build.benchpress.dart', function () { + benchpress.build({ + benchmarksPath: 'build/dart/benchmarks/lib', + buildPath: 'build/benchpress/dart' + }) +}); + +gulp.task('benchmarks/build.dart', function() { + runSequence( + 'modules/build.dart', + 'benchmarks/build.dart2js.dart', + 'benchmarks/create-bpconf.dart', + 'benchmarks/build.benchpress.dart' ); }); diff --git a/modules/benchmarks/pubspec.yaml b/modules/benchmarks/pubspec.yaml new file mode 100644 index 0000000000..6edb51ae5c --- /dev/null +++ b/modules/benchmarks/pubspec.yaml @@ -0,0 +1,10 @@ +name: bnehcmarks +environment: + sdk: '>=1.4.0' +dependencies: + facade: + path: ../facade + di: + path: ../di + core: + path: ../core diff --git a/modules/benchmarks/src/di/benchmark.dart b/modules/benchmarks/src/di/benchmark.dart new file mode 100644 index 0000000000..61c19f5371 --- /dev/null +++ b/modules/benchmarks/src/di/benchmark.dart @@ -0,0 +1,11 @@ +library injector_get_benchmark; + +import './injector_instantiate_benchmark.dart' as b; +import 'dart:js' as js; + +main () { + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "Injector.instantiate", + "fn": new js.JsFunction.withThis((_) => b.run()) + })); +} \ No newline at end of file diff --git a/modules/benchmarks/src/di/injector_instantiate_benchmark.js b/modules/benchmarks/src/di/injector_instantiate_benchmark.js index b17b289c01..9a8d44875f 100644 --- a/modules/benchmarks/src/di/injector_instantiate_benchmark.js +++ b/modules/benchmarks/src/di/injector_instantiate_benchmark.js @@ -10,8 +10,6 @@ export function run () { var child = injector.createChild([E]); child.get(E); } - - console.log(count) } class A { diff --git a/modules/benchmarks/src/element_injector/benchmark.dart b/modules/benchmarks/src/element_injector/benchmark.dart new file mode 100644 index 0000000000..edadf50d3d --- /dev/null +++ b/modules/benchmarks/src/element_injector/benchmark.dart @@ -0,0 +1,11 @@ +library element_injector_benchmark; + +import './instantiate_benchmark.dart' as ib; +import 'dart:js' as js; + +main () { + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "ElementInjector.instantiate + instantiateDirectives", + "fn": new js.JsFunction.withThis((_) => ib.run()) + })); +} \ No newline at end of file diff --git a/modules/benchmarks/src/element_injector/benchmark.es5 b/modules/benchmarks/src/element_injector/benchmark.es5 new file mode 100644 index 0000000000..874e1e0c73 --- /dev/null +++ b/modules/benchmarks/src/element_injector/benchmark.es5 @@ -0,0 +1,7 @@ +System.import('benchmarks/element_injector/instantiate_benchmark').then(function (bm) { + window.benchmarkSteps.push({name: 'ElementInjector.instantiate + instantiateDirectives', fn: bm.run}); +}, console.log.bind(console)); + +System.import('benchmarks/element_injector/instantiate_directive_benchmark').then(function (bm) { + window.benchmarkSteps.push({name: 'ElementInjector.instantiateDirectives', fn: bm.run}); +}, console.log.bind(console)); \ No newline at end of file diff --git a/modules/benchmarks/src/element_injector/bp.conf.es5 b/modules/benchmarks/src/element_injector/bp.conf.es5 new file mode 100644 index 0000000000..b9ff8a4015 --- /dev/null +++ b/modules/benchmarks/src/element_injector/bp.conf.es5 @@ -0,0 +1,11 @@ +module.exports = function(config) { + config.set({ + scripts: [ + {src: '/js/traceur-runtime.js'}, + {src: '/js/es6-module-loader-sans-promises.src.js'}, + {src: '/js/extension-register.js'}, + {src: 'register_system.js'}, + {src: 'benchmark.js'} + ] + }); +}; diff --git a/modules/benchmarks/src/element_injector/instantiate_benchmark.js b/modules/benchmarks/src/element_injector/instantiate_benchmark.js new file mode 100644 index 0000000000..df31b072c4 --- /dev/null +++ b/modules/benchmarks/src/element_injector/instantiate_benchmark.js @@ -0,0 +1,33 @@ +import {Injector} from 'di/di'; +import {ProtoElementInjector} from 'core/compiler/element_injector'; + +var count = 0; + +export function run () { + var appInjector = new Injector([]); + + var bindings = [A, B, C]; + var proto = new ProtoElementInjector(null, bindings, []); + for (var i = 0; i < 20000; ++i) { + var ei = proto.instantiate(); + ei.instantiateDirectives(appInjector); + } +} + +class A { + constructor() { + count++; + } +} + +class B { + constructor() { + count++; + } +} + +class C { + constructor(a:A, b:B) { + count++; + } +} \ No newline at end of file diff --git a/modules/benchmarks/src/element_injector/instantiate_directive_benchmark.js b/modules/benchmarks/src/element_injector/instantiate_directive_benchmark.js new file mode 100644 index 0000000000..104a0ae7c6 --- /dev/null +++ b/modules/benchmarks/src/element_injector/instantiate_directive_benchmark.js @@ -0,0 +1,35 @@ +import {Injector} from 'di/di'; +import {ProtoElementInjector} from 'core/compiler/element_injector'; + +var count = 0; + +export function run () { + var appInjector = new Injector([]); + + var bindings = [A, B, C]; + var proto = new ProtoElementInjector(null, bindings, []); + var ei = proto.instantiate(); + + for (var i = 0; i < 20000; ++i) { + ei.clearDirectives(); + ei.instantiateDirectives(appInjector); + } +} + +class A { + constructor() { + count++; + } +} + +class B { + constructor() { + count++; + } +} + +class C { + constructor(a:A, b:B) { + count++; + } +} \ No newline at end of file diff --git a/modules/benchmarks/src/element_injector/main.html b/modules/benchmarks/src/element_injector/main.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/benchmarks/src/element_injector/register_system.es5 b/modules/benchmarks/src/element_injector/register_system.es5 new file mode 100644 index 0000000000..388ec08a16 --- /dev/null +++ b/modules/benchmarks/src/element_injector/register_system.es5 @@ -0,0 +1,10 @@ +System.paths = { + 'core/*': '/js/core/lib/*.js', + 'change_detection/*': '/js/change_detection/lib/*.js', + 'facade/*': '/js/facade/lib/*.js', + 'di/*': '/js/di/lib/*.js', + 'rtts_assert/*': '/js/rtts_assert/lib/*.js', + 'test_lib/*': '/js/test_lib/lib/*.js', + 'benchmarks/*': '/js/benchmarks/lib/*.js' +}; +register(System); diff --git a/modules/core/src/annotations/visibility.js b/modules/core/src/annotations/visibility.js new file mode 100644 index 0000000000..3e2f4e6792 --- /dev/null +++ b/modules/core/src/annotations/visibility.js @@ -0,0 +1,22 @@ +import {CONST} from 'facade/lang'; +import {DependencyAnnotation} from 'di/di'; + +/** + * The directive can only be injected from the current element + * or from its parent. + */ +export class Parent extends DependencyAnnotation { + @CONST() + constructor() { + } +} + +/** + * The directive can only be injected from the current element + * or from its ancestor. + */ +export class Ancestor extends DependencyAnnotation { + @CONST() + constructor() { + } +} diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 439d0db497..cec2407890 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -1,4 +1,72 @@ -import {FIELD} from 'facade/lang'; +import {FIELD, isPresent, isBlank, Type, int} from 'facade/lang'; +import {Math} from 'facade/math'; +import {List, ListWrapper} from 'facade/collection'; +import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError} from 'di/di'; +import {Parent, Ancestor} from 'core/annotations/visibility'; + +var MAX_DEPTH = Math.pow(2, 30) - 1; + +class TreeNode { + @FIELD('_parent:TreeNode') + @FIELD('_head:TreeNode') + @FIELD('_tail:TreeNode') + @FIELD('_next:TreeNode') + @FIELD('_prev:TreeNode') + constructor(parent:TreeNode) { + this._parent = parent; + this._head = null; + this._tail = null; + this._next = null; + this._prev = null; + if (isPresent(parent)) parent._addChild(this); + } + + /** + * Adds a child to the parent node. The child MUST NOT be a part of a tree. + */ + _addChild(child:TreeNode) { + if (isPresent(this._tail)) { + this._tail._next = child; + child._prev = this._tail; + this._tail = child; + } else { + this._tail = this._head = child; + } + } + + get parent() { + return this._parent; + } + + get children() { + var res = []; + var child = this._head; + while (child != null) { + ListWrapper.push(res, child); + child = child._next; + } + return res; + } +} + +class DirectiveDependency extends Dependency { + constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List, depth:int) { + super(key, asPromise, lazy, properties); + this.depth = depth; + } + + static createFrom(d:Dependency):Dependency { + return new DirectiveDependency(d.key, d.asPromise, d.lazy, + d.properties, DirectiveDependency._depth(d.properties)); + } + + static _depth(properties):int { + if (properties.length == 0) return 0; + if (ListWrapper.any(properties, p => p instanceof Parent)) return 1; + if (ListWrapper.any(properties, p => p instanceof Ancestor)) return MAX_DEPTH; + return 0; + } +} /** @@ -18,35 +86,34 @@ ElementInjector (ElementModule): - 1:1 to DOM structure. PERF BENCHMARK: http://www.williambrownstreet.net/blog/2014/04/faster-angularjs-rendering-angularjs-and-reactjs/ - */ -export class ProtoElementInjector { +export class ProtoElementInjector extends TreeNode { /** parent:ProtoDirectiveInjector; next:ProtoDirectiveInjector; prev:ProtoDirectiveInjector; head:ProtoDirectiveInjector; tail:ProtoDirectiveInjector; - DirectiveInjector cloneingInstance; + DirectiveInjector cloningInstance; KeyMap keyMap; /// Because DI tree is sparse, this shows how far away is the Parent DI parentDistance:int = 1; /// 1 for non-sparse/normal depth. cKey:int; cFactory:Function; cParams:List; - keyId0:int; factory0:Function; params0:List; - keyId1:int; factory1:Function; params1:List; - keyId2:int; factory2:Function; params2:List; - keyId3:int; factory3:Function; params3:List; - keyId4:int; factory4:Function; params4:List; - keyId5:int; factory5:Function; params5:List; - keyId6:int; factory6:Function; params6:List; - keyId7:int; factory7:Function; params7:List; - keyId8:int; factory8:Function; params8:List; - keyId9:int; factory9:Function; params9:List; + _keyId0:int; factory0:Function; params0:List; + _keyId1:int; factory1:Function; params1:List; + _keyId2:int; factory2:Function; params2:List; + _keyId3:int; factory3:Function; params3:List; + _keyId4:int; factory4:Function; params4:List; + _keyId5:int; factory5:Function; params5:List; + _keyId6:int; factory6:Function; params6:List; + _keyId7:int; factory7:Function; params7:List; + _keyId8:int; factory8:Function; params8:List; + _keyId9:int; factory9:Function; params9:List; - queryKeyId0:int; - queryKeyId1:int; + query_keyId0:int; + query_keyId1:int; textNodes:List; hasProperties:boolean; @@ -54,17 +121,91 @@ export class ProtoElementInjector { elementInjector:ElementInjector; */ - constructor(parent:ProtoElementInjector) { + @FIELD('_elementInjector:ElementInjector') + @FIELD('_binding0:Binding') + @FIELD('_binding1:Binding') + @FIELD('_binding2:Binding') + @FIELD('_binding3:Binding') + @FIELD('_binding4:Binding') + @FIELD('_binding5:Binding') + @FIELD('_binding6:Binding') + @FIELD('_binding7:Binding') + @FIELD('_binding8:Binding') + @FIELD('_binding9:Binding') + @FIELD('_key0:int') + @FIELD('_key1:int') + @FIELD('_key2:int') + @FIELD('_key3:int') + @FIELD('_key4:int') + @FIELD('_key5:int') + @FIELD('_key6:int') + @FIELD('_key7:int') + @FIELD('_key8:int') + @FIELD('_key9:int') + @FIELD('textNodes:List') + constructor(parent:ProtoElementInjector, directiveTypes:List, textNodes:List) { + super(parent); + + this._elementInjector = null; + + this._binding0 = null; this._keyId0 = null; + this._binding1 = null; this._keyId1 = null; + this._binding2 = null; this._keyId2 = null; + this._binding3 = null; this._keyId3 = null; + this._binding4 = null; this._keyId4 = null; + this._binding5 = null; this._keyId5 = null; + this._binding6 = null; this._keyId6 = null; + this._binding7 = null; this._keyId7 = null; + this._binding8 = null; this._keyId8 = null; + this._binding9 = null; this._keyId9 = null; + + var length = directiveTypes.length; + + if (length > 0) {this._binding0 = this._createBinding(directiveTypes[0]); this._keyId0 = this._binding0.key.id;} + if (length > 1) {this._binding1 = this._createBinding(directiveTypes[1]); this._keyId1 = this._binding1.key.id;} + if (length > 2) {this._binding2 = this._createBinding(directiveTypes[2]); this._keyId2 = this._binding2.key.id;} + if (length > 3) {this._binding3 = this._createBinding(directiveTypes[3]); this._keyId3 = this._binding3.key.id;} + if (length > 4) {this._binding4 = this._createBinding(directiveTypes[4]); this._keyId4 = this._binding4.key.id;} + if (length > 5) {this._binding5 = this._createBinding(directiveTypes[5]); this._keyId5 = this._binding5.key.id;} + if (length > 6) {this._binding6 = this._createBinding(directiveTypes[6]); this._keyId6 = this._binding6.key.id;} + if (length > 7) {this._binding7 = this._createBinding(directiveTypes[7]); this._keyId7 = this._binding7.key.id;} + if (length > 8) {this._binding8 = this._createBinding(directiveTypes[8]); this._keyId8 = this._binding8.key.id;} + if (length > 9) {this._binding9 = this._createBinding(directiveTypes[9]); this._keyId9 = this._binding9.key.id;} + if (length > 10) { + throw 'Maximum number of directives per element has been reached.'; + } + + // dummy fields to make analyzer happy + this.textNodes = []; this.hasProperties = false; - this.textNodes = null; } instantiate():ElementInjector { - return new ElementInjector(this); + var p = this._parent; + var parentElementInjector = p == null ? null : p._elementInjector; + this._elementInjector = new ElementInjector({ + proto: this, + parent: parentElementInjector + }); + return this._elementInjector; + } + + _createBinding(directiveType:Type) { + var b = bind(directiveType).toClass(directiveType); + var deps = ListWrapper.map(b.dependencies, DirectiveDependency.createFrom); + return new Binding(b.key, b.factory, deps, b.providedAsPromise); + } + + clearElementInjector() { + this._elementInjector = null; + } + + get hasBindings():boolean { + return isPresent(this._binding0); } } -export class ElementInjector { +export class ElementInjector extends TreeNode { /* _protoInjector:ProtoElementInjector; injector:Injector; @@ -107,10 +248,141 @@ export class ElementInjector { _query1:Query; */ - @FIELD('final protoInjector:ProtoElementInjector') - constructor(protoInjector:ProtoElementInjector) { - this.protoInjector = protoInjector; + + @FIELD('_proto:ProtoElementInjector') + @FIELD('_appInjector:Injector') + @FIELD('_obj0:Object') + @FIELD('_obj1:Object') + @FIELD('_obj2:Object') + @FIELD('_obj3:Object') + @FIELD('_obj4:Object') + @FIELD('_obj5:Object') + @FIELD('_obj6:Object') + @FIELD('_obj7:Object') + @FIELD('_obj8:Object') + @FIELD('_obj9:Object') + constructor({proto, parent}) { + super(parent); + this._proto = proto; + + //we cannot call clearDirectives because fields won't be detected + this._appInjector = null; + this._obj0 = null; + this._obj1 = null; + this._obj2 = null; + this._obj3 = null; + this._obj4 = null; + this._obj5 = null; + this._obj6 = null; + this._obj7 = null; + this._obj8 = null; + this._obj9 = null; } + clearDirectives() { + this._appInjector = null; + this._obj0 = null; + this._obj1 = null; + this._obj2 = null; + this._obj3 = null; + this._obj4 = null; + this._obj5 = null; + this._obj6 = null; + this._obj7 = null; + this._obj8 = null; + this._obj9 = null; + } + + instantiateDirectives(appInjector:Injector) { + this._appInjector = appInjector; + + var p = this._proto; + if (isPresent(p._keyId0)) this._obj0 = this._new(p._binding0); + if (isPresent(p._keyId1)) this._obj1 = this._new(p._binding1); + if (isPresent(p._keyId2)) this._obj2 = this._new(p._binding2); + if (isPresent(p._keyId3)) this._obj3 = this._new(p._binding3); + if (isPresent(p._keyId4)) this._obj4 = this._new(p._binding4); + if (isPresent(p._keyId5)) this._obj5 = this._new(p._binding5); + if (isPresent(p._keyId6)) this._obj6 = this._new(p._binding6); + if (isPresent(p._keyId7)) this._obj7 = this._new(p._binding7); + if (isPresent(p._keyId8)) this._obj8 = this._new(p._binding8); + if (isPresent(p._keyId9)) this._obj9 = this._new(p._binding9); + } + + get(token) { + return this._getByKey(Key.get(token), 0); + } + + _new(binding:Binding) { + var factory = binding.factory; + var deps = binding.dependencies; + var length = deps.length; + + var d0,d1,d2,d3,d4,d5,d6,d7,d8,d9; + try { + d0 = length > 0 ? this._getByDependency(deps[0]) : null; + d1 = length > 1 ? this._getByDependency(deps[1]) : null; + d2 = length > 2 ? this._getByDependency(deps[2]) : null; + d3 = length > 3 ? this._getByDependency(deps[3]) : null; + d4 = length > 4 ? this._getByDependency(deps[4]) : null; + d5 = length > 5 ? this._getByDependency(deps[5]) : null; + d6 = length > 6 ? this._getByDependency(deps[6]) : null; + d7 = length > 7 ? this._getByDependency(deps[7]) : null; + d8 = length > 8 ? this._getByDependency(deps[8]) : null; + d9 = length > 9 ? this._getByDependency(deps[9]) : null; + } catch(e) { + if (e instanceof ProviderError) e.addKey(binding.key); + throw e; + } + + var obj; + switch(length) { + case 0: obj = factory(); break; + case 1: obj = factory(d0); break; + case 2: obj = factory(d0, d1); break; + case 3: obj = factory(d0, d1, d2); break; + case 4: obj = factory(d0, d1, d2, d3); break; + case 5: obj = factory(d0, d1, d2, d3, d4); break; + case 6: obj = factory(d0, d1, d2, d3, d4, d5); break; + case 7: obj = factory(d0, d1, d2, d3, d4, d5, d6); break; + case 8: obj = factory(d0, d1, d2, d3, d4, d5, d6, d7); break; + case 9: obj = factory(d0, d1, d2, d3, d4, d5, d6, d7, d8); break; + case 10: obj = factory(d0, d1, d2, d3, d4, d5, d6, d7, d8, d9); break; + default: throw `Directive ${binding.key.token} can only have up to 10 dependencies.`; + } + + return obj; + } + + _getByDependency(dep:DirectiveDependency) { + return this._getByKey(dep.key, dep.depth); + } + + _getByKey(key:Key, depth:int) { + var ei = this; + while (ei != null && depth >= 0) { + var obj = ei._getDirectiveByKey(key); + if (isPresent(obj)) return obj; + ei = ei._parent; + depth -= 1; + } + return this._appInjector.get(key); + } + + _getDirectiveByKey(key:Key) { + var p = this._proto; + var keyId= key.id; + if (p._keyId0 === keyId) return this._obj0; + if (p._keyId1 === keyId) return this._obj1; + if (p._keyId2 === keyId) return this._obj2; + if (p._keyId3 === keyId) return this._obj3; + if (p._keyId4 === keyId) return this._obj4; + if (p._keyId5 === keyId) return this._obj5; + if (p._keyId6 === keyId) return this._obj6; + if (p._keyId7 === keyId) return this._obj7; + if (p._keyId8 === keyId) return this._obj8; + if (p._keyId9 === keyId) return this._obj9; + return null; + } } diff --git a/modules/core/test/compiler/element_injector_spec.js b/modules/core/test/compiler/element_injector_spec.js new file mode 100644 index 0000000000..55f0fde6f8 --- /dev/null +++ b/modules/core/test/compiler/element_injector_spec.js @@ -0,0 +1,177 @@ +import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach} from 'test_lib/test_lib'; +import {isBlank, FIELD} from 'facade/lang'; +import {ListWrapper, MapWrapper, List} from 'facade/collection'; +import {ProtoElementInjector} from 'core/compiler/element_injector'; +import {Parent, Ancestor} from 'core/annotations/visibility'; +import {Injector, Inject, bind} from 'di/di'; + +class Directive { +} + +class NeedsDirective { + @FIELD("dependency:Directive") + constructor(dependency:Directive){ + this.dependency = dependency; + } +} + +class NeedDirectiveFromParent { + @FIELD("dependency:Directive") + constructor(@Parent() dependency:Directive){ + this.dependency = dependency; + } +} + +class NeedDirectiveFromAncestor { + @FIELD("dependency:Directive") + constructor(@Ancestor() dependency:Directive){ + this.dependency = dependency; + } +} + +class NeedsService { + @FIELD("service:Object") + constructor(@Inject("service") service) { + this.service = service; + } +} + +export function main() { + function humanize(tree, names:List) { + var lookupName = (item) => + ListWrapper.last( + ListWrapper.find(names, (pair) => pair[0] === item)); + + if (tree.children.length == 0) return lookupName(tree); + var children = tree.children.map(m => humanize(m, names)); + return [lookupName(tree), children]; + } + + describe("ElementInjector", function () { + describe("proto injectors", function () { + it("should construct a proto tree", function () { + var p = new ProtoElementInjector(null, [], []); + var c1 = new ProtoElementInjector(p, [], []); + var c2 = new ProtoElementInjector(p, [], []); + + expect(humanize(p, [ + [p, 'parent'], + [c1, 'child1'], + [c2, 'child2'] + ])).toEqual(["parent", ["child1", "child2"]]); + }); + }); + + describe("instantiate", function () { + it("should create an element injector", function () { + var protoParent = new ProtoElementInjector(null, [], []); + var protoChild1 = new ProtoElementInjector(protoParent, [], []); + var protoChild2 = new ProtoElementInjector(protoParent, [], []); + + var p = protoParent.instantiate(); + var c1 = protoChild1.instantiate(); + var c2 = protoChild2.instantiate(); + + expect(humanize(p, [ + [p, 'parent'], + [c1, 'child1'], + [c2, 'child2'] + ])).toEqual(["parent", ["child1", "child2"]]); + }); + }); + + describe("hasBindings", function () { + it("should be true when there are bindings", function () { + var p = new ProtoElementInjector(null, [Directive], []); + expect(p.hasBindings).toBeTruthy(); + }); + + it("should be false otherwise", function () { + var p = new ProtoElementInjector(null, [], []); + expect(p.hasBindings).toBeFalsy(); + }); + }); + + describe("instantiateDirectives", function () { + function injector(bindings, appInjector = null) { + var proto = new ProtoElementInjector(null, bindings, []); + var inj = proto.instantiate(); + + if (isBlank(appInjector)) appInjector = new Injector([]); + inj.instantiateDirectives(appInjector); + return inj; + } + + function parentChildInjectors(parentBindings, childBindings) { + var inj = new Injector([]); + + var protoParent = new ProtoElementInjector(null, parentBindings, []); + var parent = protoParent.instantiate(); + parent.instantiateDirectives(inj); + + var protoChild = new ProtoElementInjector(protoParent, childBindings, []); + var child = protoChild.instantiate(); + child.instantiateDirectives(inj); + + return child; + } + + it("should instantiate directives that have no dependencies", function () { + var inj = injector([Directive]); + expect(inj.get(Directive)).toBeAnInstanceOf(Directive); + }); + + it("should instantiate directives that depend on other directives", function () { + var inj = injector([Directive, NeedsDirective]); + + var d = inj.get(NeedsDirective); + + expect(d).toBeAnInstanceOf(NeedsDirective); + expect(d.dependency).toBeAnInstanceOf(Directive); + }); + + it("should instantiate directives that depend on app services", function () { + var appInjector = new Injector([ + bind("service").toValue("service") + ]); + var inj = injector([NeedsService], appInjector); + + var d = inj.get(NeedsService); + expect(d).toBeAnInstanceOf(NeedsService); + expect(d.service).toEqual("service"); + }); + + it("should return app services", function () { + var appInjector = new Injector([ + bind("service").toValue("service") + ]); + var inj = injector([], appInjector); + + expect(inj.get('service')).toEqual('service'); + }); + + it("should get directives from parent", function () { + var child = parentChildInjectors([Directive], [NeedDirectiveFromParent]); + + var d = child.get(NeedDirectiveFromParent); + + expect(d).toBeAnInstanceOf(NeedDirectiveFromParent); + expect(d.dependency).toBeAnInstanceOf(Directive); + }); + + it("should get directives from ancestor", function () { + var child = parentChildInjectors([Directive], [NeedDirectiveFromAncestor]); + + var d = child.get(NeedDirectiveFromAncestor); + + expect(d).toBeAnInstanceOf(NeedDirectiveFromAncestor); + expect(d.dependency).toBeAnInstanceOf(Directive); + }); + + it("should throw when no directive found", function () { + expect(() => injector([NeedDirectiveFromParent])). + toThrowError('No provider for Directive! (NeedDirectiveFromParent -> Directive)'); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index cf3d6ab5b6..f20cd89787 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -25,10 +25,10 @@ export function main() { '' + ''); var module:Module = null; - var sectionPI = new ProtoElementInjector(null); + var sectionPI = new ProtoElementInjector(null, null, null); sectionPI.textNodes = [0]; - var divPI = new ProtoElementInjector(null); - var spanPI = new ProtoElementInjector(null); + var divPI = new ProtoElementInjector(null, null, null); + var spanPI = new ProtoElementInjector(null, null, null); spanPI.hasProperties = true; var protoElementInjector:List = [sectionPI, divPI, spanPI]; var protoWatchGroup:ProtoWatchGroup = null; diff --git a/modules/di/src/annotations.js b/modules/di/src/annotations.js index 5ceecde1db..d364307f1e 100644 --- a/modules/di/src/annotations.js +++ b/modules/di/src/annotations.js @@ -1,5 +1,13 @@ import {CONST} from "facade/lang"; +/** + * A parameter annotation that creates a synchronous eager dependency. + * + * class AComponent { + * constructor(@Inject('aServiceToken') aService) {} + * } + * + */ export class Inject { @CONST() constructor(token) { @@ -7,6 +15,16 @@ export class Inject { } } +/** + * A parameter annotation that creates an asynchronous eager dependency. + * + * class AComponent { + * constructor(@InjectPromise('aServiceToken') aServicePromise) { + * aServicePromise.then(aService => ...); + * } + * } + * + */ export class InjectPromise { @CONST() constructor(token) { @@ -14,9 +32,48 @@ export class InjectPromise { } } +/** + * A parameter annotation that creates a synchronous lazy dependency. + * + * class AComponent { + * constructor(@InjectLazy('aServiceToken') aServiceFn) { + * aService = aServiceFn(); + * } + * } + * + */ export class InjectLazy { @CONST() constructor(token) { this.token = token; } } + +/** + * `DependencyAnnotation` is used by the framework to extend DI. + * + * Only annotations implementing `DependencyAnnotation` will be added + * to the list of dependency properties. + * + * For example: + * + * class Parent extends DependencyAnnotation {} + * class NotDependencyProperty {} + * + * class AComponent { + * constructor(@Parent @NotDependencyProperty aService:AService) {} + * } + * + * will create the following dependency: + * + * new Dependency(Key.get(AService), [new Parent()]) + * + * The framework can use `new Parent()` to handle the `aService` dependency + * in a specific way. + * + */ +export class DependencyAnnotation { + @CONST() + constructor() { + } +} \ No newline at end of file diff --git a/modules/di/src/binding.js b/modules/di/src/binding.js index 60d0c7031a..8261bd8696 100644 --- a/modules/di/src/binding.js +++ b/modules/di/src/binding.js @@ -7,10 +7,11 @@ export class Dependency { @FIELD('final key:Key') @FIELD('final asPromise:bool') @FIELD('final lazy:bool') - constructor(key:Key, asPromise:boolean, lazy:boolean) { + constructor(key:Key, asPromise:boolean, lazy:boolean, properties:List) { this.key = key; this.asPromise = asPromise; this.lazy = lazy; + this.properties = properties; } } @@ -44,7 +45,7 @@ export class BindingBuilder { toValue(value):Binding { return new Binding( Key.get(this.token), - (_) => value, + () => value, [], false ); @@ -53,7 +54,7 @@ export class BindingBuilder { toFactory(factoryFunction:Function, dependencies:List = null):Binding { return new Binding( Key.get(this.token), - reflector.convertToFactory(factoryFunction), + factoryFunction, this._constructDependencies(factoryFunction, dependencies), false ); @@ -62,7 +63,7 @@ export class BindingBuilder { toAsyncFactory(factoryFunction:Function, dependencies:List = null):Binding { return new Binding( Key.get(this.token), - reflector.convertToFactory(factoryFunction), + factoryFunction, this._constructDependencies(factoryFunction, dependencies), true ); @@ -71,6 +72,6 @@ export class BindingBuilder { _constructDependencies(factoryFunction:Function, dependencies:List) { return isBlank(dependencies) ? reflector.dependencies(factoryFunction) : - ListWrapper.map(dependencies, (t) => new Dependency(Key.get(t), false, false)); + ListWrapper.map(dependencies, (t) => new Dependency(Key.get(t), false, false, [])); } } diff --git a/modules/di/src/di.js b/modules/di/src/di.js index 942592ba5b..1f380b60de 100644 --- a/modules/di/src/di.js +++ b/modules/di/src/di.js @@ -3,3 +3,4 @@ export * from './injector'; export * from './binding'; export * from './key'; export * from './module'; +export * from './exceptions'; diff --git a/modules/di/src/injector.js b/modules/di/src/injector.js index a20a184189..3dba34e167 100644 --- a/modules/di/src/injector.js +++ b/modules/di/src/injector.js @@ -5,6 +5,7 @@ import {ProviderError, NoProviderError, InvalidBindingError, import {Type, isPresent, isBlank} from 'facade/lang'; import {Promise, PromiseWrapper} from 'facade/async'; import {Key} from './key'; +import {reflector} from './reflector'; var _constructing = new Object(); @@ -150,7 +151,7 @@ class _SyncInjectorStrategy { _createInstance(key:Key, binding:Binding, deps:List) { try { - var instance = binding.factory(deps); + var instance = reflector.invoke(binding.factory, deps); this.injector._setInstance(key, instance); return instance; } catch (e) { @@ -211,7 +212,7 @@ class _AsyncInjectorStrategy { try { var instance = this.injector._getInstance(key); if (!_isWaiting(instance)) return instance; - return binding.factory(deps); + return reflector.invoke(binding.factory, deps); } catch (e) { this.injector._clear(key); throw new InstantiationError(e, key); diff --git a/modules/di/src/reflector.dart b/modules/di/src/reflector.dart index bb6657726e..40720b8cf0 100644 --- a/modules/di/src/reflector.dart +++ b/modules/di/src/reflector.dart @@ -1,26 +1,49 @@ library facade.di.reflector; import 'dart:mirrors'; -import 'annotations.dart' show Inject, InjectPromise, InjectLazy; +import 'annotations.dart' show Inject, InjectPromise, InjectLazy, DependencyAnnotation; import 'key.dart' show Key; import 'binding.dart' show Dependency; import 'exceptions.dart' show NoAnnotationError; class Reflector { Function factoryFor(Type type) { - return _generateFactory(type); - } - - Function convertToFactory(Function factory) { - return (args) => Function.apply(factory, args); - } - - Function _generateFactory(Type type) { ClassMirror classMirror = reflectType(type); MethodMirror ctor = classMirror.declarations[classMirror.simpleName]; Function create = classMirror.newInstance; Symbol name = ctor.constructorName; - return (args) => create(name, args).reflectee; + int length = ctor.parameters.length; + + switch (length) { + case 0: return () => + create(name, []).reflectee; + case 1: return (a1) => + create(name, [a1]).reflectee; + case 2: return (a1, a2) => + create(name, [a1, a2]).reflectee; + case 3: return (a1, a2, a3) => + create(name, [a1, a2, a3]).reflectee; + case 4: return (a1, a2, a3, a4) => + create(name, [a1, a2, a3, a4]).reflectee; + case 5: return (a1, a2, a3, a4, a5) => + create(name, [a1, a2, a3, a4, a5]).reflectee; + case 6: return (a1, a2, a3, a4, a5, a6) => + create(name, [a1, a2, a3, a4, a5, a6]).reflectee; + case 7: return (a1, a2, a3, a4, a5, a6, a7) => + create(name, [a1, a2, a3, a4, a5, a6, a7]).reflectee; + case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => + create(name, [a1, a2, a3, a4, a5, a6, a7, a8]).reflectee; + case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => + create(name, [a1, a2, a3, a4, a5, a6, a7, a8, a9]).reflectee; + case 10: return (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) => + create(name, [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]).reflectee; + }; + + throw "Factory cannot take more than 10 arguments"; + } + + invoke(Function factory, List args) { + return Function.apply(factory, args); } List dependencies(typeOrFunc) { @@ -38,16 +61,17 @@ class Reflector { var injectLazy = metadata.firstWhere((m) => m is InjectLazy, orElse: () => null); if (inject != null) { - return new Dependency(Key.get(inject.token), false, false); + return new Dependency(Key.get(inject.token), false, false, []); } else if (injectPromise != null) { - return new Dependency(Key.get(injectPromise.token), true, false); + return new Dependency(Key.get(injectPromise.token), true, false, []); } else if (injectLazy != null) { - return new Dependency(Key.get(injectLazy.token), false, true); + return new Dependency(Key.get(injectLazy.token), false, true, []); } else if (p.type.qualifiedName != #dynamic) { - return new Dependency(Key.get(p.type.reflectedType), false, false); + var depProps = metadata.where((m) => m is DependencyAnnotation).toList(); + return new Dependency(Key.get(p.type.reflectedType), false, false, depProps); } else { throw new NoAnnotationError(typeOrFunc); diff --git a/modules/di/src/reflector.es6 b/modules/di/src/reflector.es6 index ad62b904ab..a2af6874e0 100644 --- a/modules/di/src/reflector.es6 +++ b/modules/di/src/reflector.es6 @@ -1,17 +1,43 @@ import {Type, isPresent} from 'facade/lang'; import {List} from 'facade/collection'; -import {Inject, InjectPromise, InjectLazy} from './annotations'; +import {Inject, InjectPromise, InjectLazy, DependencyAnnotation} from './annotations'; import {Key} from './key'; import {Dependency} from './binding'; import {NoAnnotationError} from './exceptions'; class Reflector { factoryFor(type:Type):Function { - return (args) => new type(...args); + var length = type.parameters ? type.parameters.length : 0; + switch (length) { + case 0: return () => + new type(); + case 1: return (a1) => + new type(a1); + case 2: return (a1, a2) => + new type(a1, a2); + case 3: return (a1, a2, a3) => + new type(a1, a2, a3); + case 4: return (a1, a2, a3, a4) => + new type(a1, a2, a3, a4); + case 5: return (a1, a2, a3, a4, a5) => + new type(a1, a2, a3, a4, a5); + case 6: return (a1, a2, a3, a4, a5, a6) => + new type(a1, a2, a3, a4, a5, a6); + case 7: return (a1, a2, a3, a4, a5, a6, a7) => + new type(a1, a2, a3, a4, a5, a6, a7); + case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => + new type(a1, a2, a3, a4, a5, a6, a7, a8); + case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => + new type(a1, a2, a3, a4, a5, a6, a7, a8, a9); + case 10: return (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) => + new type(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); + }; + + throw "Factory cannot take more than 10 arguments"; } - convertToFactory(factoryFunction:Function):Function { - return (args) => factoryFunction(...args); + invoke(factory:Function, args:List) { + return factory(...args); } dependencies(typeOrFunc):List { @@ -23,31 +49,35 @@ class Reflector { _extractToken(typeOrFunc, annotations) { var type; + var depProps = []; for (var paramAnnotation of annotations) { if (paramAnnotation instanceof Type) { type = paramAnnotation; } else if (paramAnnotation instanceof Inject) { - return this._createDependency(paramAnnotation.token, false, false); + return this._createDependency(paramAnnotation.token, false, false, []); } else if (paramAnnotation instanceof InjectPromise) { - return this._createDependency(paramAnnotation.token, true, false); + return this._createDependency(paramAnnotation.token, true, false, []); } else if (paramAnnotation instanceof InjectLazy) { - return this._createDependency(paramAnnotation.token, false, true); + return this._createDependency(paramAnnotation.token, false, true, []); + + } else if (paramAnnotation instanceof DependencyAnnotation) { + depProps.push(paramAnnotation); } } if (isPresent(type)) { - return this._createDependency(type, false, false); + return this._createDependency(type, false, false, depProps); } else { throw new NoAnnotationError(typeOrFunc); } } - _createDependency(token, asPromise, lazy):Dependency { - return new Dependency(Key.get(token), asPromise, lazy); + _createDependency(token, asPromise, lazy, depProps):Dependency { + return new Dependency(Key.get(token), asPromise, lazy, depProps); } } diff --git a/modules/di/test/di/reflector_spec.js b/modules/di/test/di/reflector_spec.js new file mode 100644 index 0000000000..fd6fec6599 --- /dev/null +++ b/modules/di/test/di/reflector_spec.js @@ -0,0 +1,23 @@ +import {ddescribe, describe, it, iit, expect} from 'test_lib/test_lib'; +import {Key, Inject, DependencyAnnotation} from 'di/di'; +import {CONST} from 'facade/lang'; +import {reflector, Token} from 'di/reflector'; + +class Parent extends DependencyAnnotation { + @CONST() + constructor() { + } +} + +export function main() { + describe("reflector", function () { + describe("dependencies", function () { + it('should collect annotations implementing DependencyAnnotation as properties', function () { + function f(@Parent() arg:Function) {} + + var dep = reflector.dependencies(f)[0]; + expect(dep.properties[0]).toBeAnInstanceOf(Parent); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 8844b25d2b..5c5530b053 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -22,6 +22,8 @@ class ListWrapper { static void set(m, k, v) { m[k] = v; } static contains(m, k) => m.containsKey(k); static map(list, fn) => list.map(fn).toList(); + static find(List list, fn) => list.firstWhere(fn, orElse:() => null); + static any(List list, fn) => list.any(fn); static forEach(list, fn) { list.forEach(fn); } diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 2195127f10..b3ef85167d 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -41,6 +41,18 @@ export class ListWrapper { if (!array || array.length == 0) return null; return array[array.length - 1]; } + static find(list:List, pred:Function) { + for (var i = 0 ; i < list.length; ++i) { + if (pred(list[i])) return list[i]; + } + return null; + } + static any(list:List, pred:Function) { + for (var i = 0 ; i < list.length; ++i) { + if (pred(list[i])) return true; + } + return false; + } static reversed(array) { var a = ListWrapper.clone(array); return a.reverse(); diff --git a/modules/facade/src/math.dart b/modules/facade/src/math.dart new file mode 100644 index 0000000000..f4b952821f --- /dev/null +++ b/modules/facade/src/math.dart @@ -0,0 +1,9 @@ +library angular.core.facade.math; + +import 'dart:math' as math; + +class Math { + static num pow(num x, num exponent) { + return math.pow(x, exponent); + } +} diff --git a/modules/facade/src/math.es6 b/modules/facade/src/math.es6 new file mode 100644 index 0000000000..e85edbda98 --- /dev/null +++ b/modules/facade/src/math.es6 @@ -0,0 +1 @@ +export var Math = window.Math; \ No newline at end of file diff --git a/package.json b/package.json index 8f67a6ae71..9f6f10e1e1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "gulp": "^3.8.8", "gulp-rename": "^1.2.0", "gulp-watch": "^1.0.3", + "gulp-shell": "^0.2.10", "karma-cli": "^0.0.4", "karma": "^0.12.23", "karma-chrome-launcher": "^0.1.4",