From 8cad231bd219eddd518de8b8bd040d3f12f08d17 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 18 Apr 2011 16:33:30 -0700 Subject: Refactor injector to have invoke method for speed reasons --- src/AngularPublic.js | 2 + src/Compiler.js | 10 +-- src/Injector.js | 171 ++++++++++++++++++++++++++++++--------------------- src/Scope.js | 4 +- src/apis.js | 34 +++++++++- src/directives.js | 4 +- src/widgets.js | 4 +- 7 files changed, 145 insertions(+), 84 deletions(-) (limited to 'src') 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.=} [providers=angular.service] Map of provider (factory) - * function. - * @param {Object.=} [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.=} [factories=angular.service] Map of service factory + * functions. + * @param {Object.=} [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}. + * + *
+ * var MyController = angular.annotate('$location', function($location){ ... });
+ * 
+ * + * is the same as + * + *
+ * var MyController = function($location){ ... };
+ * MyController.$inject = ['$location'];
+ * 
+ * + * @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){ */ 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) { */ 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; -- cgit v1.2.3