aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2011-04-18 16:33:30 -0700
committerMisko Hevery2011-06-08 15:21:31 -0700
commit8cad231bd219eddd518de8b8bd040d3f12f08d17 (patch)
tree75016fa6de683a877916f45a0fd06b1d0f312231
parent0e17ade959cc77369dc102d180e43be2af68505a (diff)
downloadangular.js-8cad231bd219eddd518de8b8bd040d3f12f08d17.tar.bz2
Refactor injector to have invoke method for speed reasons
-rw-r--r--CHANGELOG.md7
-rwxr-xr-x[-rw-r--r--]docs/src/gen-docs.js0
-rw-r--r--src/AngularPublic.js2
-rw-r--r--src/Compiler.js10
-rw-r--r--src/Injector.js171
-rw-r--r--src/Scope.js4
-rw-r--r--src/apis.js34
-rw-r--r--src/directives.js4
-rw-r--r--src/widgets.js4
-rw-r--r--test/InjectorSpec.js57
-rw-r--r--test/testabilityPatch.js2
11 files changed, 182 insertions, 113 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 333b153a..25bffca4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@
### Bug Fixes
- Number filter would return incorrect value when fractional part had leading zeros.
+### Breaking changes
+- $service now has $service.invoke for method injection ($service(self, fn) no longer works)
+- injection name inference no longer supports method curry and linking functions. Both must be
+ explicitly specified using $inject property.
+
<a name="0.9.16"><a/>
# <angular/> 0.9.16 weather-control (2011-06-07) #
@@ -526,4 +531,4 @@ with the `$route` service
[guide.di]: http://docs.angularjs.org/#!/guide/dev_guide.di
[downloading]: http://docs.angularjs.org/#!/misc/downloading
[contribute]: http://docs.angularjs.org/#!/misc/contribute
-[Jstd Scenario Adapter]: https://github.com/angular/angular.js/blob/master/src/jstd-scenario-adapter/Adapter.js \ No newline at end of file
+[Jstd Scenario Adapter]: https://github.com/angular/angular.js/blob/master/src/jstd-scenario-adapter/Adapter.js
diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js
index ead01c32..ead01c32 100644..100755
--- a/docs/src/gen-docs.js
+++ b/docs/src/gen-docs.js
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index cc901d6d..37e0a082 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -18,6 +18,8 @@ angularService('$browser', function($log){
}, {$inject:['$log']});
extend(angular, {
+ // disabled for now until we agree on public name
+ //'annotate': annotate,
'element': jqLite,
'compile': compile,
'scope': createScope,
diff --git a/src/Compiler.js b/src/Compiler.js
index d5eeae79..08c76eea 100644
--- a/src/Compiler.js
+++ b/src/Compiler.js
@@ -34,7 +34,7 @@ Template.prototype = {
forEach(this.inits, function(fn) {
queue.push(function() {
childScope.$tryEval(function(){
- return childScope.$service(fn, childScope, element);
+ return childScope.$service.invoke(childScope, fn, [element]);
}, element);
});
});
@@ -49,9 +49,11 @@ Template.prototype = {
},
- addInit:function(init) {
- if (init) {
- this.inits.push(init);
+ addInit:function(linkingFn) {
+ if (linkingFn) {
+ if (!linkingFn.$inject)
+ linkingFn.$inject = [];
+ this.inits.push(linkingFn);
}
},
diff --git a/src/Injector.js b/src/Injector.js
index 77d25a77..9e1464e9 100644
--- a/src/Injector.js
+++ b/src/Injector.js
@@ -4,81 +4,114 @@
* @function
*
* @description
- * Creates an inject function that can be used for dependency injection.
- * (See {@link guide/dev_guide.di dependency injection})
+ * Creates an injector function that can be used for retrieving services as well as for
+ * dependency injection (see {@link guide/dev_guide.di dependency injection}).
*
- * The inject function can be used for retrieving service instances or for calling any function
- * which has the $inject property so that the services can be automatically provided. Angular
- * creates an injection function automatically for the root scope and it is available as
- * {@link angular.scope.$service $service}.
+ * Angular creates an injector automatically for the root scope and it is available as the
+ * {@link angular.scope.$service $service} property. Creation of the injector automatically creates
+ * all of the `$eager` {@link angular.service services}.
*
- * @param {Object=} [providerScope={}] provider's `this`
- * @param {Object.<string, function()>=} [providers=angular.service] Map of provider (factory)
- * function.
- * @param {Object.<string, function()>=} [cache={}] Place where instances are saved for reuse. Can
- * also be used to override services speciafied by `providers` (useful in tests).
- * @returns
- * {function()} Injector function: `function(value, scope, args...)`:
+ * @param {Object=} [factoryScope={}] `this` for the service factory function.
+ * @param {Object.<string, function()>=} [factories=angular.service] Map of service factory
+ * functions.
+ * @param {Object.<string, function()>=} [instanceCache={}] Place where instances of services are
+ * saved for reuse. Can also be used to override services specified by `serviceFactory`
+ * (useful in tests).
+ * @returns {function()} Injector function:
*
- * * `value` - `{string|array|function}`
- * * `scope(optional=rootScope)` - optional function "`this`" when `value` is type `function`.
- * * `args(optional)` - optional set of arguments to pass to function after injection arguments.
- * (also known as curry arguments or currying).
+ * * `injector(serviceName)`:
+ * * `serviceName` - `{string=}` - name of the service to retrieve.
*
- * #Return value of `function(value, scope, args...)`
- * The injector function return value depended on the type of `value` argument:
- *
- * * `string`: return an instance for the injection key.
- * * `array` of keys: returns an array of instances for those keys. (see `string` above.)
- * * `function`: look at `$inject` property of function to determine instances to inject
- * and then call the function with instances and `scope`. Any additional arguments
- * (`args`) are appended to the function arguments.
- * * `none`: initialize eager providers.
+ * The injector function also has these properties:
*
+ * * an `invoke` property which can be used to invoke methods with dependency-injected arguments.
+ * `injector.invoke(self, fn, curryArgs)`
+ * * `self` - "`this`" to be used when invoking the function.
+ * * `fn` - the function to be invoked. The function may have the `$inject` property which
+ * lists the set of arguments which should be auto injected
+ * (see {@link guide.di dependency injection}).
+ * * `curryArgs(array)` - optional array of arguments to pass to function invocation after the
+ * injection arguments (also known as curry arguments or currying).
+ * * an `eager` property which is used to initialize the eager services.
+ * `injector.eager()`
*/
-function createInjector(providerScope, providers, cache) {
- providers = providers || angularService;
- cache = cache || {};
- providerScope = providerScope || {};
- return function inject(value, scope, args){
- var returnValue, provider;
- if (isString(value)) {
- if (!(value in cache)) {
- provider = providers[value];
- if (!provider) throw "Unknown provider for '"+value+"'.";
- cache[value] = inject(provider, providerScope);
- }
- returnValue = cache[value];
- } else if (isArray(value)) {
- returnValue = [];
- forEach(value, function(name) {
- returnValue.push(inject(name));
- });
- } else if (isFunction(value)) {
- returnValue = inject(injectionArgs(value));
- returnValue = value.apply(scope, concat(returnValue, arguments, 2));
- } else if (isObject(value)) {
- forEach(providers, function(provider, name){
- if (provider.$eager)
- inject(name);
+function createInjector(factoryScope, factories, instanceCache) {
+ factories = factories || angularService;
+ instanceCache = instanceCache || {};
+ factoryScope = factoryScope || {};
+ injector.invoke = invoke;
- if (provider.$creation)
- throw new Error("Failed to register service '" + name +
- "': $creation property is unsupported. Use $eager:true or see release notes.");
- });
- } else {
- returnValue = inject(providerScope);
+ injector.eager = function(){
+ forEach(factories, function(factory, name){
+ if (factory.$eager)
+ injector(name);
+
+ if (factory.$creation)
+ throw new Error("Failed to register service '" + name +
+ "': $creation property is unsupported. Use $eager:true or see release notes.");
+ });
+ };
+ return injector;
+
+ function injector(value){
+ if (!(value in instanceCache)) {
+ var factory = factories[value];
+ if (!factory) throw Error("Unknown provider for '"+value+"'.");
+ instanceCache[value] = invoke(factoryScope, factory);
}
- return returnValue;
+ return instanceCache[value];
};
-}
-function injectService(services, fn) {
- return extend(fn, {$inject:services});
+ function invoke(self, fn, args){
+ args = args || [];
+ var injectNames = injectionArgs(fn);
+ var i = injectNames.length;
+ while(i--) {
+ args.unshift(injector(injectNames[i]));
+ }
+ return fn.apply(self, args);
+ }
}
-function injectUpdateView(fn) {
- return injectService(['$updateView'], fn);
+/*NOT_PUBLIC_YET
+ * @ngdoc function
+ * @name angular.annotate
+ * @function
+ *
+ * @description
+ * Annotate the function with injection arguments. This is equivalent to setting the `$inject`
+ * property as described in {@link guide.di dependency injection}.
+ *
+ * <pre>
+ * var MyController = angular.annotate('$location', function($location){ ... });
+ * </pre>
+ *
+ * is the same as
+ *
+ * <pre>
+ * var MyController = function($location){ ... };
+ * MyController.$inject = ['$location'];
+ * </pre>
+ *
+ * @param {String|Array} serviceName... zero or more service names to inject into the
+ * `annotatedFunction`.
+ * @param {function} annotatedFunction function to annotate with `$inject`
+ * functions.
+ * @returns {function} `annotatedFunction`
+ */
+function annotate(services, fn) {
+ if (services instanceof Array) {
+ fn.$inject = services;
+ return fn;
+ } else {
+ var i = 0,
+ length = arguments.length - 1, // last one is the destination function
+ $inject = arguments[length].$inject = [];
+ for (; i < length; i++) {
+ $inject.push(arguments[i]);
+ }
+ return arguments[length]; // return the last one
+ }
}
function angularServiceInject(name, fn, inject, eager) {
@@ -89,12 +122,12 @@ function angularServiceInject(name, fn, inject, eager) {
/**
* @returns the $inject property of function. If not found the
* the $inject is computed by looking at the toString of function and
- * extracting all arguments which start with $ or end with _ as the
+ * extracting all arguments which and assuming that they are the
* injection names.
*/
var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/;
var FN_ARG_SPLIT = /,/;
-var FN_ARG = /^\s*(((\$?).+?)(_?))\s*$/;
+var FN_ARG = /^\s*(.+?)\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function injectionArgs(fn) {
assertArgFn(fn);
@@ -103,12 +136,8 @@ function injectionArgs(fn) {
var fnText = fn.toString().replace(STRIP_COMMENTS, '');
var argDecl = fnText.match(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
- arg.replace(FN_ARG, function(all, name, injectName, $, _){
- assertArg(args, name, 'after non-injectable arg');
- if ($ || _)
- args.push(injectName);
- else
- args = null; // once we reach an argument which is not injectable then ignore
+ arg.replace(FN_ARG, function(all, name){
+ args.push(name);
});
});
}
diff --git a/src/Scope.js b/src/Scope.js
index 1ab583e8..b2b8cdb4 100644
--- a/src/Scope.js
+++ b/src/Scope.js
@@ -472,7 +472,7 @@ function createScope(parent, providers, instanceCache) {
forEach(Class.prototype, function(fn, name){
instance[name] = bind(instance, fn);
});
- instance.$service.apply(instance, concat([Class, instance], arguments, 1));
+ instance.$service.invoke(instance, Class, slice.call(arguments, 1, arguments.length));
//TODO: backwards compatibility hack, remove when we don't depend on init methods
if (isFunction(Class.prototype.init)) {
@@ -525,7 +525,7 @@ function createScope(parent, providers, instanceCache) {
* @param {string} serviceId String ID of the service to return.
* @returns {*} Value, object or function returned by the service factory function if any.
*/
- (instance.$service = createInjector(instance, providers, instanceCache))();
+ instance.$service = createInjector(instance, providers, instanceCache);
}
$log = instance.$service('$log');
diff --git a/src/apis.js b/src/apis.js
index 11fa9838..1e1bcae1 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -807,6 +807,16 @@ var angularFunction = {
}
};
+/**
+ * Computes a hash of an 'obj'.
+ * Hash of a:
+ * string is string
+ * number is number as string
+ * object is either call $hashKey function on object or assign unique hashKey id.
+ *
+ * @param obj
+ * @returns {String} hash string such that the same input will have the same hash string
+ */
function hashKey(obj) {
var objType = typeof obj;
var key = obj;
@@ -821,17 +831,37 @@ function hashKey(obj) {
return objType + ':' + key;
}
+/**
+ * HashMap which can use objects as keys
+ */
function HashMap(){}
HashMap.prototype = {
+ /**
+ * Store key value pair
+ * @param key key to store can be any type
+ * @param value value to store can be any type
+ * @returns old value if any
+ */
put: function(key, value) {
var _key = hashKey(key);
var oldValue = this[_key];
this[_key] = value;
return oldValue;
},
+
+ /**
+ * @param key
+ * @returns the value for the key
+ */
get: function(key) {
return this[hashKey(key)];
},
+
+ /**
+ * Remove the key/value pair
+ * @param key
+ * @returns value associated with key before it was removed
+ */
remove: function(key) {
var _key = hashKey(key);
var value = this[_key];
@@ -852,8 +882,6 @@ defineApi('Array', [angularGlobal, angularCollection, angularArray]);
defineApi('Object', [angularGlobal, angularCollection, angularObject]);
defineApi('String', [angularGlobal, angularString]);
defineApi('Date', [angularGlobal, angularDate]);
-// TODO: enable and document this API
-//defineApi('HashMap', [HashMap]);
//IE bug
-angular['Date']['toString'] = angularDate['toString'];
+angular.Date.toString = angularDate.toString;
defineApi('Function', [angularGlobal, angularCollection, angularFunction]);
diff --git a/src/directives.js b/src/directives.js
index 680e8a72..34a1b27d 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -511,7 +511,7 @@ angularDirective("ng:bind-attr", function(expression){
* TODO: maybe we should consider allowing users to control event propagation in the future.
*/
angularDirective("ng:click", function(expression, element){
- return injectUpdateView(function($updateView, element){
+ return annotate('$updateView', function($updateView, element){
var self = this;
element.bind('click', function(event){
self.$tryEval(expression, element);
@@ -561,7 +561,7 @@ angularDirective("ng:click", function(expression, element){
</doc:example>
*/
angularDirective("ng:submit", function(expression, element) {
- return injectUpdateView(function($updateView, element) {
+ return annotate('$updateView', function($updateView, element) {
var self = this;
element.bind('submit', function(event) {
self.$tryEval(expression, element);
diff --git a/src/widgets.js b/src/widgets.js
index cc6d48b9..4245f99c 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -522,7 +522,7 @@ function radioInit(model, view, element) {
</doc:example>
*/
function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
- return injectService(['$updateView', '$defer'], function($updateView, $defer, element) {
+ return annotate('$updateView', '$defer', function($updateView, $defer, element) {
var scope = this,
model = modelAccessor(scope, element),
view = viewAccessor(scope, element),
@@ -1097,7 +1097,7 @@ angularWidget('ng:view', function(element) {
if (!element[0]['ng:compiled']) {
element[0]['ng:compiled'] = true;
- return injectService(['$xhr.cache', '$route'], function($xhr, $route, element){
+ return annotate('$xhr.cache', '$route', function($xhr, $route, element){
var parentScope = this,
childScope;
diff --git a/test/InjectorSpec.js b/test/InjectorSpec.js
index 84e515b7..8db854af 100644
--- a/test/InjectorSpec.js
+++ b/test/InjectorSpec.js
@@ -1,29 +1,23 @@
describe('injector', function(){
var providers;
var cache;
- var inject;
+ var injector;
var scope;
beforeEach(function(){
providers = extensionMap({}, 'providers');
cache = {};
scope = {};
- inject = createInjector(scope, providers, cache);
+ injector = createInjector(scope, providers, cache);
});
it("should return same instance from calling provider", function(){
providers('text', function(){ return scope.name; });
scope.name = 'abc';
- expect(inject('text')).toEqual('abc');
+ expect(injector('text')).toEqual('abc');
expect(cache.text).toEqual('abc');
scope.name = 'deleted';
- expect(inject('text')).toEqual('abc');
- });
-
- it("should return an array of instances", function(){
- cache.a = 0;
- providers('b', function(){return 2;});
- expect(inject(['a', 'b'])).toEqual([0,2]);
+ expect(injector('text')).toEqual('abc');
});
it("should call function", function(){
@@ -34,14 +28,14 @@ describe('injector', function(){
args = [this, a, b, c, d];
}
fn.$inject = ['a', 'b'];
- inject(fn, {name:"this"}, 3, 4);
+ injector.invoke({name:"this"}, fn, [3, 4]);
expect(args).toEqual([{name:'this'}, 1, 2, 3, 4]);
});
it('should inject providers', function(){
providers('a', function(){return this.mi = 'Mi';});
providers('b', function(mi){return this.name = mi+'sko';}, {$inject:['a']});
- expect(inject('b')).toEqual('Misko');
+ expect(injector('b')).toEqual('Misko');
expect(scope).toEqual({mi:'Mi', name:'Misko'});
});
@@ -65,24 +59,24 @@ describe('injector', function(){
providers('s5', function(){ log.push('s5'); });
providers('s6', function(){ log.push('s6'); });
- inject('s1');
+ injector('s1');
- expect(log).toEqual(['s6', 's3', 's5', 's4', 's2', 's1']);
+ expect(log).toEqual(['s6', 's5', 's3', 's4', 's2', 's1']);
});
it('should provide usefull message if no provider', function(){
- assertThrows("Unknown provider for 'idontexist'.", function(){
- inject('idontexist');
- });
+ expect(function(){
+ injector('idontexist');
+ }).toThrow("Unknown provider for 'idontexist'.");
});
it('should autostart eager services', function(){
var log = '';
providers('eager', function(){log += 'eager;'; return 'foo';}, {$eager: true});
- inject();
+ injector.eager();
expect(log).toEqual('eager;');
- expect(inject('eager')).toBe('foo');
+ expect(injector('eager')).toBe('foo');
});
describe('annotation', function(){
@@ -105,9 +99,10 @@ describe('injector', function(){
multi-line comment
function (a, b){}
*/
- /* {some type} */ c){ extraParans();}
- expect(injectionArgs($f_n0)).toEqual(['$a', 'b']);
- expect($f_n0.$inject).toEqual(['$a', 'b']);
+ _c,
+ /* {some type} */ d){ extraParans();}
+ expect(injectionArgs($f_n0)).toEqual(['$a', 'b_', '_c', 'd']);
+ expect($f_n0.$inject).toEqual(['$a', 'b_', '_c', 'd']);
});
it('should handle no arg functions', function(){
@@ -118,8 +113,8 @@ describe('injector', function(){
it('should handle args with both $ and _', function(){
function $f_n0($a_){}
- expect(injectionArgs($f_n0)).toEqual(['$a']);
- expect($f_n0.$inject).toEqual(['$a']);
+ expect(injectionArgs($f_n0)).toEqual(['$a_']);
+ expect($f_n0.$inject).toEqual(['$a_']);
});
it('should throw on non function arg', function(){
@@ -128,11 +123,17 @@ describe('injector', function(){
}).toThrow();
});
- it('should throw on injectable after non-injectable arg', function(){
- expect(function(){
- injectionArgs(function($a, b_, nonInject, d_){});
- }).toThrow();
+ });
+
+ describe('inject', function(){
+ it('should inject names', function(){
+ expect(angular.annotate('a', {}).$inject).toEqual(['a']);
+ expect(angular.annotate('a', 'b', {}).$inject).toEqual(['a', 'b']);
});
+ it('should inject array', function(){
+ expect(angular.annotate(['a'], {}).$inject).toEqual(['a']);
+ expect(angular.annotate(['a', 'b'], {}).$inject).toEqual(['a', 'b']);
+ });
});
});
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 4545f937..0776f620 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -156,6 +156,7 @@ function dealoc(obj) {
}
extend(angular, {
+ 'annotate': annotate,
'element': jqLite,
'compile': compile,
'scope': createScope,
@@ -168,6 +169,7 @@ extend(angular, {
'toJson': toJson,
'fromJson': fromJson,
'identity':identity,
+ 'injector': createInjector,
'isUndefined': isUndefined,
'isDefined': isDefined,
'isString': isString,