Deconstructing AngularJS Injector

I was reading through Coders at Workand realized that I haven’t done much code reading lately, other than reading through code samples in tutorials and articles. The interviews in the book suggested that code reviews and code reading are one of the most important things you can do as a professional programmer. This gave me the idea to write this article on the angular injector.

I’ve been doing some stuff with AngularJS and one of the key ideas is dependency injection. Here’s a deconstruction of injector.js which contains all the functions and classes for dependency injection.

The Perils of Long Explanations and Examples in Comments

Before deconstructing the code, I had to figure out where the actual code was. The reason for this is that AngularJS’s developers tried to be clever and create their own custom documentation system. It looks kinda like JavaDocs, and it will extract the comments into a separate API docs file. The problem is that there is far fewer code in injector.js than comments, and the comments are longer explanations that are more fit for a tutorial or a manual than for a quick API reference, in other words, they belong in the docs/ folder as a text file.

The other problem with these comments is that they’re far away from their corresponding code. For example, there is a comment on the “get” method of $injector from lines 130-140 while the code is at lines 710-725, and line 786 (“get” is an alias for the “getService” method).

The original injector.js is 794 lines of code. When the comments outside of functions are removed, we’re down to 254 lines of code, click here to see the difference.

Contracts or Static Types Would Help

The biggest problem I have with dynamically typed languages is that there are no type hints written out. With AngularJS’s implementation of dependency injection, the dependencies are passed around as the first items in a list followed by the function which dependencies on them. Instead of talking about them as functions (which have no dependency) or arrays where the last item is a function or calling it “array notation”, it would be helpful to create a type that represents them. Not a class, but a type. This would give it a name and make it easier to refer to.

Alternatively, contracts could be written for functions and methods that would state how to make a valid call to a function.

Being able to state function(x,y) requires that x is an Object that has the method “helloWorld” would be fantastic. It would save a lot of time reading through code. I know that not everyone does this, but Angular, at least in injector.js, is fond of creating objects that are plain Objects that don’t inherit or extend from other classes.

I guess this is why unit tests exist, but I find stating the contract right where the code is to be far more useful than referring back and forth between the code and the unit tests.

I’ll give an example here of what I mean:

function hello(name: String): void {
  console.log('Hello ' + name + '!');
}

function factory(name: String, factoryFn: Function): FactoryObject {
  // ...
}

function provider(name: String, provider_: FactoryObject or DIFunction): FactoryObject or DIFunction {
  // provider_ is an object or a function that
  /  uses dependency injection notation
}

Where the type FactoryObject is defined as an object that contains the $get method, and the  DIFunction type is just a function or a list where the last item is a function. It would be much clearer to me to see what’s expected when using a function.

Anyway, let’s get to the deconstruction of injector.js…

Deconstruction: injector.js

Function: $injectorMinErr

Line 45 defines a new function using minErr called “$injectorMinErr”, the function returns an Error. The “$injector” argument is used as a prefix when errors are thrown. At line 593, in “createInjector”, the “$injectorMinErr” function is called, the arguments are: the type of error, the format string, and anything after that are variables to use in the format string. The format string uses {0}, {1}, … {N} for substituting variables. You can view the docs and code for minErr here.

Function: annotate

Lines 41-44 are used by the “annotate” function (line 46). They’re regexps, regular expressions.

FN_ARGS searches for a multi-line string that matches the text “function” and has a match group for function arguments. For example, it would match “function hello(first_name, last_name) { … }”, and the match would be “first_name, last_name”.

FN_ARG_SPLIT is used to split the arguments list into separate arguments, as an example, “first_name, last_name” would be split into the array [“first_name”, “last_name”].

FN_ARG has two match groups, one for the underscore and the other for the rest of the name of the argument. For example, for “_param” the match groups would be “_” and “param”.

Line 46-74 is the “annotate” function. The only argument it accepts is “fn”, a function or an array. It returns a list of dependencies, stored in the local variable $inject.

If “fn” is a function, we check to see if it has dependencies stored in “fn.$inject” as a list, at line 53. If the dependencies aren’t already stored, some cleverness happens. When “toString” is called on a function, the code for the function is returned and the code itself is anazlyed. The function name and arguments are extracted straight from the string representation of the function, using the previously defined FN_ARGS, FN_ARG_SPLIT, FN_ARG and STRIP_COMMENTS (lines 56-59). The arguments have any leading underscore removed (using FN_ARG). All function arguments are pushed into the dependency list, $inject.

Lines 66-72, if an array or other object is passed in, it uses “assertArgFn” to ensure that “fn”  uses the array notation for a function, where last item in the array is a function, and the arguments up until the last argument are dependencies that need to be injected. If “fn” is some other object, an error is thrown by “assertArgFn”. If the array is valid, the items 1 to N-1 are stored in $inject, at line 69.

Function: createInjector

Line 576 defines the createInjector function. It accepts one argument, “modulesToLoad” which is an array of module names to load. It returns “instanceInjector” which is an object returned from the “createInternalInjector” function.

Line 580, the modules that have been loaded are stored in “loadedModules”. These are

The createInjector function has quite a few internally defined functions, the power of closures and first-class functions at work.

These internal functions are:

  • supportObject(delegate)
  • provider(name, provider_)
  • factory(name, factoryFn)
  • service(name, constructor)
  • value(name, val)
  • constant(name, value)
  • decorator(serviceName, decorFn)
  • loadModules(modulesToLoad)
  • createInternalInjector(cache, factory)

Lines 581-590, the “providerCache” is created with a single property, $provide which contains references to various internal functions.

In line 595, the “instanceCache” is created.

Lines 591-594, the “providerInjector” is created and set as the providerCache’s $injector property. The factory method passed to “createInternalInjector” throws an error if the provider doesn’t already exist in the cache.

Contrast this with lines 596-600, the “instanceInjector” is created and set as the instanceCache’s $injector property. The factory method passed to “createInternalInjector” retrieves an existing provider (using the providerInjector’s get method) and then returns an instance created using the provider’s $get method (line 599).

Line 603, we’re looping through each module in “modulesToLoad” and using the the instanceInjector to invoke each module function.

We’ll go through the internal functions in order.

createInjector.supportObject

Line 611, the “supportObject” internal function accepts a function as an argument, “delegate”. The “supportObject” function is currying, it returns a function which accepts two arguments, key and value. When key is an object (see isObject), it loops through each property key/value (using forEach and reverseParams) of the object and calls the “delegate” function with the property key and value.

Otherwise, the key isn’t an Object and we pass the key and value argument to the “delegate” function.

Example:

createInjector.provider

Line 621, the “provider” internal function accepts two arguments, “name” and “provider_”. “Name” is a string, while “provider_” is either a function, an array, or a Provider object. When “provider_” is an array it’s actually a function with dependencies and uses dependency injection (lines 623-624). If it’s a Provider object, then it needs a “$get” factory method (lines 626-627).

Line 622 ensures that the “name” doesn’t have a “service” property set.

The “provider” function adds the “provider_” to the provider cache and returns the now-cached provider, line 629.

createInjector.factory

Line 632, the “factory” internal function accepts a “name” and a “factoryFn”. “Name” is a string, while “factoryFn” is a factory function. The function calls “provider” and passes both arguments to it (the factory function is used as the “$get” property of a new object).

createInjector.service

Line 634, the “service” internal function accepts a “name” and “constructor”. It returns the the result of calling the “factory” internal function, passing “name” and a factory function (a function with dependencies) to it. The factory function depends on the $injector and passes the “constructor” function to $injector.instantiate (defined on line 770, part of the object returned from createInternalInjector).

createInjector.value

Line 640, the “value” internal function returns the result of calling the “factory” internal function, passing in the “name” (a string) and the “val”.  The “val” argument is wrapped in “valueFn” which returns a function that returns the given value, see line 383 in Angular.js.

Here’s an example of how to use valueFn:

and here’s how the “value” internal function works:

createInjector.constant

Line 642, the “constant” internal function takes two arguments, “name” and “value”. It stores the “value” in the $injector’s providerCache and the instanceCache. The “name” argument should be a string, while the “value” can be any type of object.

createInjector.decorator

Line 648, the “decorator” internal function takes two arguments, “serviceName” and “decorFn”. The “serviceName” is a string, it’s joined/concatenated with the providerSuffix so that the provider can be retrieved from the providerInjector (line 649). The $get method of the provider is stored (line 650), so that it can be decorated (similar to how Python decorators work) in lines 652-654. The instanceInjector and its “invoke” method is used (defined later on, line 727). The decorated $get function returns the result of invoking the decorator function, “decorFn” with the original $get method passed in as a $delegate to “invoke”.

createInjector.loadModules

Line 661-702 defines the “loadModules” internal function. It accepts one argument, an array of modules to load. It loops through each item in the array, and if it’s a string it uses angularModule to find the existing module function to load. It also finds all of the modules that the module requires (stored in the “requires” property), lines 668-677. If the item is a function or array (a function with dependencies), there’s no need to search for an existing module, it’s used as is.

The checks for each item are wrapped in a try-catch block which throws an error (using the $injectorMinErr function) that displays the stack trace, lines 685-698.

createInjector.createInternalInjector

Finally we get to the good stuff, line 708 to the end of the file, the function that actually creates an injector (at least internally). The function accepts a cache object and a factory function.

It contains the following inner functions:

  • getService(serviceName)
  • invoke(fn, self, locals)
  • instantiate(Type, locals)
createInternalInjector.getService

Lines 710-725, the “getService” function returns a service from the cache passed to createInternalInjector. If the service doesn’t exist yet, the factory function passed to createInternalInjector is used to create the service and the cache is updated.

5 Comments to Deconstructing AngularJS Injector

  1. Rudolf Olah says:

    Reblogged this on GoAugust and commented:

    a deconstruction of the angularjs injector code, very interesting how it’s structured

  2. […] Deconstructing AngularJS Injector […]

  3. […] Deconstructing AngularJS Injector […]