邹明潮 Angular Dependency Injection

邹明潮
KevinZou
Published in
7 min readApr 12, 2017

Dependency Injection

Dependency injection is a glue that holds every other components together. It has three different level as the followings are shown:

  • Injector
  • Provider
  • High-Level Dependency Injection Features

Injector

Registering modules

  • Singleton
  var ensure = function(obj, name, factory) {
return obj[name] || (obj[name] = factory());
};
//Function Programming
ensure(angular, 'module', function() {
var modules = {};
return function(name, requires) {
return createModule(name, requires, modules);
};
});
  • Invoke Queue

What the module should hold is a collection of tasks such as “register a constant” that the injector should carry out when it loads the module. This collection of tasks is called the invoke queue, that is, an array of arrays. Each array in the queue has two items: The type of application component that should be registered, and the arguments for registering that component.

var createModule = function(name, requires, modules) {
var invokeQueue = [];
var invokeLater = function(method, arrayMethod) {
return function() {
invokeQueue[arrayMethod || 'push']([method, arguments]);
return moduleInstance;
};
};
var moduleInstance = {
name: name,
requires: requires,
constant: invokeLater('constant', 'unshift'),
provider: invokeLater('provider'),
_invokeQueue: invokeQueue
}
modules[name] = moduleInstance;
return moduleInstance;
};

Instantiating modules

  • Loading ‘requires’ recursively: depth first
  • Circular dependency: an object in which we keep track of the modules that have been loaded
function createInjector(modulesToLoad) {
var providerCache = {};
var instanceCache = {};
var loadedModules = {};
providerCache.$provide = {
constant: function(key, value) {
providerCache[key] = value;
instanceCache[key] = value;
},
provider: function(key, provider) {
providerCache[key + 'Provider'] = provider;
}
};
_.forEach(modulesToLoad, function loadModule(moduleName) {
if (!loadedModules.hasOwnProperty(moduleName)) {
loadedModules[moduleName] = true; //mark
var module = window.angular.module(moduleName);
_.forEach(module.requires, loadModule); //DFS
// carry out tasks stored in the invoke queue

_.forEach(module._invokeQueue, function(invokeArgs) {
var method = invokeArgs[0];
var args = invokeArgs[1];
providerCache.$provide[method].apply(providerCache.$provide, args);
});
}
});
}

Dependency Injection

The real purpose of the injector is to do actual dependency injection, that is, invoke the functions, construct objects and automatically look up the dependencies they need.

function annotate(fn) {
if (_.isArray(fn)) {
return fn.slice(0, fn.length - 1);
} else if (fn.$inject) {
return fn.$inject;
} else if (!fn.length) {
return [];
} else { //
var source = fn.toString().replace(STRIP_COMMENTS, '');
var argDeclaration = source.match(FN_ARGS);
return _.map(argDeclaration[1].split(','), function(argName) {
return argName.match(FN_ARG)[2];
});
}
}
/*
* self:object(this explicit binding)
* locals:an object of local mappings from dependency names to values
*/
function invoke(fn, self, locals) {
var args = _.map(annotate(fn), function(token) {
if (_.isString(token)) {
return locals && locals.hasOwnProperty(token) ?
locals[token] :
getService(token);
} else {
throw 'Incorrect injection token! Expected a string;
}
});
if (_.isArray(fn)) {
fn = _.last(fn);
}
return fn.apply(self, args);
}
function instantiate(Type, locals) {
var UnwrappedType = _.isArray(Type) ? _.last(Type) : Type;
var instance = Object.create(UnwrappedType.prototype);
invoke(Type, instance, locals);
return instance;
}

Use case:

  1. Using an attribute called $inject attached to the function. That attribute can hold an array of the names of the function’s dependencies.
var fn = function(one, two) {return one + two;}
fn.$inject = [‘a’, ‘b’];

2. Array-Style dependency injection: the last item in the array is a funtion.

var fn = ['a', 'b', function() { }];

3. Extract the dependency names from the arguments of a function

Warning: the argument names of your functions change when you run a minifier such as Closure Compiler.

//extract the arguments
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
//capture the non-whitespace section
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
//take off comments
var STRIP_COMMENTS = /(\/\/.*$)|(\/\*.*?\*\/)/mg;
var fn = function(a, b) { };
expect(injector.annotate(fn)).toEqual(['a', 'b']);

4. When you have a constructor function and want to instantiate an object

function Type(a, b) {
this.result = a + b;
}
var instance = injector.instantiate(Type, {b: 3});

Providers

What Angular calls a provider is any JavaScript object that has a method attached to it called $get. When you give such an object to the injector, it will call that $get method and treat its return value as the actual dependency.

Lazy Instantiation of Dependencies

Consider the following case, where b depends on a, but b is registered before a. In order to solve this problem, the injector invokes those $get method lazily using cache, only when their return values are needed.

Circular Dependencies

  • As we construct a dependency, before invoking its $get method, we’ll put a special marker value into the instance cache. If, then, at some point we see this marker value when looking up a dependency, which means we’re trying to look up something that we’re also currently constructing and we have a circle.
  • Use a stack called path to keep track of the relationship of dependencies.
var INSTANTIATING = {} // a special marker
var path = []; // stack
function getService(name) {
if (instanceCache.hasOwnProperty(name)) {
if (instanceCache[name] === INSTANTIATING) {
throw new Error('Circular dependency found: ' +
name + ' <- ' + path.join(' <- '));
}
return instanceCache[name];
}else if (providerCache.hasOwnProperty(name)) {
return providerCache[name];
}else if (providerCache.hasOwnProperty(name + 'Provider')) {
path.unshift(name);
instanceCache[name] = INSTANTIATING;

try {
var provider = providerCache[name + 'Provider'];
//invoke() will call getService() recursively.
var instance = invoke(provider.$get, provider);
instanceCache[name] = instance;

return instance;
} finally {
path.shift();
//make sure we don't leave the marker in if the invocation fails
if (instanceCache[name] === INSTANTIATING) {
delete instanceCache[name];
}
}
}
}

Provider Constructors

The work we did for lazy initialization does not apply to the constructor injection.

provider: function(key, provider) {
if (_.isFunction(provider)) { // constructor
provider = providerInjector.instantiate(provider);
}
providerCache[key + 'Provider'] = provider;
}

Separation(Refactoring)

Having two separate injectors: One that deals exclusively with providers, and another that deals exclusively with instances. The latter will be the one exposed through the public API, and the former will only be used internally inside createInjector.

function createInjector(modulesToLoad) {  var providerCache = {};
var providerInjector = providerCache.$injector =
createInternalInjector(providerCache, function() {
throw 'Unknown provider: '+path.join(' <- ');
});
var instanceCache = {};
var instanceInjector = instanceCache.$injector =
createInternalInjector(instanceCache, function(name) {
var provider = providerInjector.get(name + 'Provider');
return instanceInjector.invoke(provider.$get, provider);
});
providerCache.$provide = {
constant: function(key, value) {
if (key === 'hasOwnProperty') {
throw 'hasOwnProperty is not a valid constant name!';
}
providerCache[key] = value;
instanceCache[key] = value;
},
provider: function(key, provider) {
if (_.isFunction(provider)) {
provider = providerInjector.instantiate(provider);
}
providerCache[key + 'Provider'] = provider;
}
};
/*
* cache: do dependency lookups from
* factoryFn: a factory function to fall back to when there's nothing * in the cache
*/
function createInternalInjector(cache, factoryFn) {
function getService(name) {
if (cache.hasOwnProperty(name)) {
if (cache[name] === INSTANTIATING) {
throw new Error('Circular dependency found: ' +
name + ' <- ' + path.join(' <- '));
}
return cache[name];
} else {
path.unshift(name);
cache[name] = INSTANTIATING;
try {
return (cache[name] = factoryFn(name));
} finally {
path.shift();
if (cache[name] === INSTANTIATING) {
delete cache[name];
}
}
}
return {
has: function(name) {
return cache.hasOwnProperty(name) ||
providerCache.hasOwnProperty(name + 'Provider');
},
get: getService,
invoke: invoke
}
}
return instanceInjector;
}

The two injectors we now have implement two different phases of dependency injection.
1. Provider injection happens when providers are registered from a module’s invoke queue. After that, there will be no more changes to providerCache.
2. At runtime there’s instance injection, which happens whenever someone calls the injector’s external API. The instance cache is populated as dependencies are instantiated, which happens in the fallback function of instanceInjector.

Unshifting Constants in the invoke queue

  • Instances: constructing instances lazily has the nice property that it frees the application developer from having to register things in the order of their dependencies. You can register A after B, even if A has a dependency on B.
  • constructors: since provider constructors are invoked when the provider is registered (when the invoke queue is processed), you actually do need to register A before B if BProvider has a dependency to AProvider.
  • constants: since constants cannot depend on anything else, the module loader always adds them to the front of the invoke queue.
var invokeLater = function(method, arrayMethod) { 
return function() {
invokeQueue[arrayMethod || 'push']([method, arguments]);
return moduleInstance;
};
};

var moduleInstance = {
name: name,
requires: requires,
constant: invokeLater('constant', 'unshift'),
}

High-Level Dependency Injection Features

Hash Keys and Hash Maps

  • hashKey: this function takes any JavaScript value and returns a string “hash key” which is made of two parts: the first part designates the type of the value and the second part designates the value’s string representation.
function hashKey(value) {
var type = typeof value;
var uid;
if (type === 'function' || (type === 'object' && value !== null)) {
uid = value.$$hashKey;
// plug in your own behavior for generating hash keys for objects
if (typeof uid === 'function') {
uid = value.$$hashKey();
} else if (uid === undefined) {
//use the unique id generator function that comes with LoDash
uid = value.$$hashKey = _.uniqueId();
}
} else { // non-object
uid = value;
}
return type + ':' + uid;
}
  • HashMap
function HashMap() {
}
HashMap.prototype = {
put: function(key, value) {
this[hashKey(key)] = value;
},
get: function(key) {
return this[hashKey(key)];
},
remove: function(key) {
key = hashKey(key);
var value = this[key];
delete this[key];
return value;
}
};
  • Application: convert the loadedModules variable from an object literal to a HashMap instance
var loadedModules = new HashMap();

_.forEach(modulesToLoad, function loadModule(module) {
if (!loadedModules.get(module)) {
loadedModules.put(module, true);
if (_.isString(module)) {
module = window.angular.module(module);
_.forEach(module.requires, loadModule);
runInvokeQueue(module._invokeQueue);
runInvokeQueue(module._configBlocks);
runBlocks = runBlocks.concat(module._runBlocks);
} else if (_.isFunction(module) || _.isArray(module)) {
runBlocks.push(providerInjector.invoke(module));
}
}
});

Decorators

You use a decorator to modify some existing dependency.

//Override the provider's $get method
decorator: function(serviceName, decoratorFn) {
var provider = providerInjector.get(serviceName + 'Provider');
var original$get = provider.$get;
provider.$get = function() {
var instance = instanceInjector.invoke(original$get, provider);
instanceInjector.invoke(decoratorFn, null, {$delegate: instance});
return instance;
};
}
//Test case
it('uses dependency injection with decorators', function() {
var module = window.angular.module('myModule', []);
module.factory('aValue', function() {
return {};
});
module.constant('a', 42);
module.decorator('aValue', function(a, $delegate) {
$delegate.decoratedKey = a;
});
var injector = createInjector(['myModule']);
expect(injector.get('aValue').decoratedKey).toBe(42);
});

--

--