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
- 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
[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']});
extend(angular, {
// disabled for now until we agree on public name
//'annotate': annotate,
'element': jqLite,
'compile': compile,
'scope': createScope,

View file

@ -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);
}
},

View file

@ -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);
}
return returnValue;
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 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) {
return extend(fn, {$inject:services});
}
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);
});
});
}

View file

@ -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');

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) {
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]);

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.
*/
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);

View file

@ -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;

View file

@ -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']);
});
});
});

View file

@ -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,