Refactor injector to have invoke method for speed reasons

This commit is contained in:
Misko Hevery 2011-04-18 16:33:30 -07:00
parent 0e17ade959
commit 8cad231bd2
11 changed files with 184 additions and 115 deletions

View file

@ -4,6 +4,11 @@
### Bug Fixes ### Bug Fixes
- Number filter would return incorrect value when fractional part had leading zeros. - 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/> <a name="0.9.16"><a/>
# <angular/> 0.9.16 weather-control (2011-06-07) # # <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 [guide.di]: http://docs.angularjs.org/#!/guide/dev_guide.di
[downloading]: http://docs.angularjs.org/#!/misc/downloading [downloading]: http://docs.angularjs.org/#!/misc/downloading
[contribute]: http://docs.angularjs.org/#!/misc/contribute [contribute]: http://docs.angularjs.org/#!/misc/contribute
[Jstd Scenario Adapter]: https://github.com/angular/angular.js/blob/master/src/jstd-scenario-adapter/Adapter.js [Jstd Scenario Adapter]: https://github.com/angular/angular.js/blob/master/src/jstd-scenario-adapter/Adapter.js

0
docs/src/gen-docs.js Normal file → Executable file
View file

View file

@ -18,6 +18,8 @@ angularService('$browser', function($log){
}, {$inject:['$log']}); }, {$inject:['$log']});
extend(angular, { extend(angular, {
// disabled for now until we agree on public name
//'annotate': annotate,
'element': jqLite, 'element': jqLite,
'compile': compile, 'compile': compile,
'scope': createScope, 'scope': createScope,

View file

@ -34,7 +34,7 @@ Template.prototype = {
forEach(this.inits, function(fn) { forEach(this.inits, function(fn) {
queue.push(function() { queue.push(function() {
childScope.$tryEval(function(){ childScope.$tryEval(function(){
return childScope.$service(fn, childScope, element); return childScope.$service.invoke(childScope, fn, [element]);
}, element); }, element);
}); });
}); });
@ -49,9 +49,11 @@ Template.prototype = {
}, },
addInit:function(init) { addInit:function(linkingFn) {
if (init) { if (linkingFn) {
this.inits.push(init); if (!linkingFn.$inject)
linkingFn.$inject = [];
this.inits.push(linkingFn);
} }
}, },

View file

@ -4,81 +4,114 @@
* @function * @function
* *
* @description * @description
* Creates an inject function that can be used for dependency injection. * Creates an injector function that can be used for retrieving services as well as for
* (See {@link guide/dev_guide.di dependency injection}) * 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 * Angular creates an injector automatically for the root scope and it is available as the
* which has the $inject property so that the services can be automatically provided. Angular * {@link angular.scope.$service $service} property. Creation of the injector automatically creates
* creates an injection function automatically for the root scope and it is available as * all of the `$eager` {@link angular.service services}.
* {@link angular.scope.$service $service}.
* *
* @param {Object=} [providerScope={}] provider's `this` * @param {Object=} [factoryScope={}] `this` for the service factory function.
* @param {Object.<string, function()>=} [providers=angular.service] Map of provider (factory) * @param {Object.<string, function()>=} [factories=angular.service] Map of service factory
* function. * functions.
* @param {Object.<string, function()>=} [cache={}] Place where instances are saved for reuse. Can * @param {Object.<string, function()>=} [instanceCache={}] Place where instances of services are
* also be used to override services speciafied by `providers` (useful in tests). * saved for reuse. Can also be used to override services specified by `serviceFactory`
* @returns * (useful in tests).
* {function()} Injector function: `function(value, scope, args...)`: * @returns {function()} Injector function:
* *
* * `value` - `{string|array|function}` * * `injector(serviceName)`:
* * `scope(optional=rootScope)` - optional function "`this`" when `value` is type `function`. * * `serviceName` - `{string=}` - name of the service to retrieve.
* * `args(optional)` - optional set of arguments to pass to function after injection arguments.
* (also known as curry arguments or currying).
* *
* #Return value of `function(value, scope, args...)` * The injector function also has these properties:
* 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.
* *
* * 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) { function createInjector(factoryScope, factories, instanceCache) {
providers = providers || angularService; factories = factories || angularService;
cache = cache || {}; instanceCache = instanceCache || {};
providerScope = providerScope || {}; factoryScope = factoryScope || {};
return function inject(value, scope, args){ injector.invoke = invoke;
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);
if (provider.$creation) injector.eager = function(){
throw new Error("Failed to register service '" + name + forEach(factories, function(factory, name){
"': $creation property is unsupported. Use $eager:true or see release notes."); if (factory.$eager)
}); injector(name);
} else {
returnValue = inject(providerScope); if (factory.$creation)
} throw new Error("Failed to register service '" + name +
return returnValue; "': $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 instanceCache[value];
};
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 injectService(services, fn) { /*NOT_PUBLIC_YET
return extend(fn, {$inject:services}); * @ngdoc function
} * @name angular.annotate
* @function
function injectUpdateView(fn) { *
return injectService(['$updateView'], fn); * @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) { 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 * @returns the $inject property of function. If not found the
* the $inject is computed by looking at the toString of function and * 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. * injection names.
*/ */
var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/; var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/;
var FN_ARG_SPLIT = /,/; var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(((\$?).+?)(_?))\s*$/; var FN_ARG = /^\s*(.+?)\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function injectionArgs(fn) { function injectionArgs(fn) {
assertArgFn(fn); assertArgFn(fn);
@ -103,12 +136,8 @@ function injectionArgs(fn) {
var fnText = fn.toString().replace(STRIP_COMMENTS, ''); var fnText = fn.toString().replace(STRIP_COMMENTS, '');
var argDecl = fnText.match(FN_ARGS); var argDecl = fnText.match(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
arg.replace(FN_ARG, function(all, name, injectName, $, _){ arg.replace(FN_ARG, function(all, name){
assertArg(args, name, 'after non-injectable arg'); args.push(name);
if ($ || _)
args.push(injectName);
else
args = null; // once we reach an argument which is not injectable then ignore
}); });
}); });
} }

View file

@ -472,7 +472,7 @@ function createScope(parent, providers, instanceCache) {
forEach(Class.prototype, function(fn, name){ forEach(Class.prototype, function(fn, name){
instance[name] = bind(instance, fn); 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 //TODO: backwards compatibility hack, remove when we don't depend on init methods
if (isFunction(Class.prototype.init)) { if (isFunction(Class.prototype.init)) {
@ -525,7 +525,7 @@ function createScope(parent, providers, instanceCache) {
* @param {string} serviceId String ID of the service to return. * @param {string} serviceId String ID of the service to return.
* @returns {*} Value, object or function returned by the service factory function if any. * @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'); $log = instance.$service('$log');

View file

@ -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) { function hashKey(obj) {
var objType = typeof obj; var objType = typeof obj;
var key = obj; var key = obj;
@ -821,17 +831,37 @@ function hashKey(obj) {
return objType + ':' + key; return objType + ':' + key;
} }
/**
* HashMap which can use objects as keys
*/
function HashMap(){} function HashMap(){}
HashMap.prototype = { 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) { put: function(key, value) {
var _key = hashKey(key); var _key = hashKey(key);
var oldValue = this[_key]; var oldValue = this[_key];
this[_key] = value; this[_key] = value;
return oldValue; return oldValue;
}, },
/**
* @param key
* @returns the value for the key
*/
get: function(key) { get: function(key) {
return this[hashKey(key)]; return this[hashKey(key)];
}, },
/**
* Remove the key/value pair
* @param key
* @returns value associated with key before it was removed
*/
remove: function(key) { remove: function(key) {
var _key = hashKey(key); var _key = hashKey(key);
var value = this[_key]; var value = this[_key];
@ -852,8 +882,6 @@ defineApi('Array', [angularGlobal, angularCollection, angularArray]);
defineApi('Object', [angularGlobal, angularCollection, angularObject]); defineApi('Object', [angularGlobal, angularCollection, angularObject]);
defineApi('String', [angularGlobal, angularString]); defineApi('String', [angularGlobal, angularString]);
defineApi('Date', [angularGlobal, angularDate]); defineApi('Date', [angularGlobal, angularDate]);
// TODO: enable and document this API
//defineApi('HashMap', [HashMap]);
//IE bug //IE bug
angular['Date']['toString'] = angularDate['toString']; angular.Date.toString = angularDate.toString;
defineApi('Function', [angularGlobal, angularCollection, angularFunction]); defineApi('Function', [angularGlobal, angularCollection, angularFunction]);

View file

@ -511,7 +511,7 @@ angularDirective("ng:bind-attr", function(expression){
* TODO: maybe we should consider allowing users to control event propagation in the future. * TODO: maybe we should consider allowing users to control event propagation in the future.
*/ */
angularDirective("ng:click", function(expression, element){ angularDirective("ng:click", function(expression, element){
return injectUpdateView(function($updateView, element){ return annotate('$updateView', function($updateView, element){
var self = this; var self = this;
element.bind('click', function(event){ element.bind('click', function(event){
self.$tryEval(expression, element); self.$tryEval(expression, element);
@ -561,7 +561,7 @@ angularDirective("ng:click", function(expression, element){
</doc:example> </doc:example>
*/ */
angularDirective("ng:submit", function(expression, element) { angularDirective("ng:submit", function(expression, element) {
return injectUpdateView(function($updateView, element) { return annotate('$updateView', function($updateView, element) {
var self = this; var self = this;
element.bind('submit', function(event) { element.bind('submit', function(event) {
self.$tryEval(expression, element); self.$tryEval(expression, element);

View file

@ -522,7 +522,7 @@ function radioInit(model, view, element) {
</doc:example> </doc:example>
*/ */
function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) { 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, var scope = this,
model = modelAccessor(scope, element), model = modelAccessor(scope, element),
view = viewAccessor(scope, element), view = viewAccessor(scope, element),
@ -1097,7 +1097,7 @@ angularWidget('ng:view', function(element) {
if (!element[0]['ng:compiled']) { if (!element[0]['ng:compiled']) {
element[0]['ng:compiled'] = true; 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, var parentScope = this,
childScope; childScope;

View file

@ -1,29 +1,23 @@
describe('injector', function(){ describe('injector', function(){
var providers; var providers;
var cache; var cache;
var inject; var injector;
var scope; var scope;
beforeEach(function(){ beforeEach(function(){
providers = extensionMap({}, 'providers'); providers = extensionMap({}, 'providers');
cache = {}; cache = {};
scope = {}; scope = {};
inject = createInjector(scope, providers, cache); injector = createInjector(scope, providers, cache);
}); });
it("should return same instance from calling provider", function(){ it("should return same instance from calling provider", function(){
providers('text', function(){ return scope.name; }); providers('text', function(){ return scope.name; });
scope.name = 'abc'; scope.name = 'abc';
expect(inject('text')).toEqual('abc'); expect(injector('text')).toEqual('abc');
expect(cache.text).toEqual('abc'); expect(cache.text).toEqual('abc');
scope.name = 'deleted'; scope.name = 'deleted';
expect(inject('text')).toEqual('abc'); expect(injector('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]);
}); });
it("should call function", function(){ it("should call function", function(){
@ -34,14 +28,14 @@ describe('injector', function(){
args = [this, a, b, c, d]; args = [this, a, b, c, d];
} }
fn.$inject = ['a', 'b']; 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]); expect(args).toEqual([{name:'this'}, 1, 2, 3, 4]);
}); });
it('should inject providers', function(){ it('should inject providers', function(){
providers('a', function(){return this.mi = 'Mi';}); providers('a', function(){return this.mi = 'Mi';});
providers('b', function(mi){return this.name = mi+'sko';}, {$inject:['a']}); 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'}); expect(scope).toEqual({mi:'Mi', name:'Misko'});
}); });
@ -65,24 +59,24 @@ describe('injector', function(){
providers('s5', function(){ log.push('s5'); }); providers('s5', function(){ log.push('s5'); });
providers('s6', function(){ log.push('s6'); }); 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(){ it('should provide usefull message if no provider', function(){
assertThrows("Unknown provider for 'idontexist'.", function(){ expect(function(){
inject('idontexist'); injector('idontexist');
}); }).toThrow("Unknown provider for 'idontexist'.");
}); });
it('should autostart eager services', function(){ it('should autostart eager services', function(){
var log = ''; var log = '';
providers('eager', function(){log += 'eager;'; return 'foo';}, {$eager: true}); providers('eager', function(){log += 'eager;'; return 'foo';}, {$eager: true});
inject(); injector.eager();
expect(log).toEqual('eager;'); expect(log).toEqual('eager;');
expect(inject('eager')).toBe('foo'); expect(injector('eager')).toBe('foo');
}); });
describe('annotation', function(){ describe('annotation', function(){
@ -105,9 +99,10 @@ describe('injector', function(){
multi-line comment multi-line comment
function (a, b){} function (a, b){}
*/ */
/* {some type} */ c){ extraParans();} _c,
expect(injectionArgs($f_n0)).toEqual(['$a', 'b']); /* {some type} */ d){ extraParans();}
expect($f_n0.$inject).toEqual(['$a', 'b']); 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(){ it('should handle no arg functions', function(){
@ -118,8 +113,8 @@ describe('injector', function(){
it('should handle args with both $ and _', function(){ it('should handle args with both $ and _', function(){
function $f_n0($a_){} function $f_n0($a_){}
expect(injectionArgs($f_n0)).toEqual(['$a']); expect(injectionArgs($f_n0)).toEqual(['$a_']);
expect($f_n0.$inject).toEqual(['$a']); expect($f_n0.$inject).toEqual(['$a_']);
}); });
it('should throw on non function arg', function(){ it('should throw on non function arg', function(){
@ -128,11 +123,17 @@ describe('injector', function(){
}).toThrow(); }).toThrow();
}); });
it('should throw on injectable after non-injectable arg', function(){ });
expect(function(){
injectionArgs(function($a, b_, nonInject, d_){}); describe('inject', function(){
}).toThrow(); 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']);
});
}); });
}); });

View file

@ -156,6 +156,7 @@ function dealoc(obj) {
} }
extend(angular, { extend(angular, {
'annotate': annotate,
'element': jqLite, 'element': jqLite,
'compile': compile, 'compile': compile,
'scope': createScope, 'scope': createScope,
@ -168,6 +169,7 @@ extend(angular, {
'toJson': toJson, 'toJson': toJson,
'fromJson': fromJson, 'fromJson': fromJson,
'identity':identity, 'identity':identity,
'injector': createInjector,
'isUndefined': isUndefined, 'isUndefined': isUndefined,
'isDefined': isDefined, 'isDefined': isDefined,
'isString': isString, 'isString': isString,