angular-cn/packages/docs/di/di.md

7.0 KiB

Dependency Injection (DI): Documentation

This document describes in detail how the DI module works in Angular.

Core Abstractions

The library is built on top of the following core abstractions: Injector, Binding, and Dependency.

  • An injector is created from a set of bindings.
  • An injector resolves dependencies and creates objects.
  • A binding maps a token, such as a string or class, to a factory function and a list of dependencies. So a binding defines how to create an object.
  • A dependency points to a token and contains extra information on how the object corresponding to that token should be injected.
[Injector]
    |
    |
    |*
[Binding]
   |----------|-----------------|
   |          |                 |*
[Token]    [FactoryFn]     [Dependency]
                               |---------|
                               |         |
                            [Token]   [Flags]

Example

class Engine {
}

class Car {
  constructor(@Inject(Engine) engine) {
  }
}

var inj = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine)
]);
var car = inj.get(Car);

In this example we create two bindings: one for Car and one for Engine. @Inject(Engine) declares a dependency on Engine.

Injector

An injector instantiates objects lazily, only when asked for, and then caches them.

Compare

var car = inj.get(Car); //instantiates both an Engine and a Car

with

var engine = inj.get(Engine); //instantiates an Engine
var car = inj.get(Car); //instantiates a Car (reuses Engine)

and with

var car = inj.get(Car); //instantiates both an Engine and a Car
var engine = inj.get(Engine); //reads the Engine from the cache

To avoid bugs make sure the registered objects have side-effect-free constructors. In this case, an injector acts like a hash map, where the order in which the objects got created does not matter.

Child Injectors and Dependencies

Injectors are hierarchical.

var parent = Injector.resolveAndCreate([
  bind(Engine).toClass(TurboEngine)
]);
var child = parent.resolveAndCreateChild([Car]);

var car = child.get(Car); // uses the Car binding from the child injector and Engine from the parent injector.

Injectors form a tree.

  GrandParentInjector
   /              \
Parent1Injector  Parent2Injector
  |
ChildInjector

The dependency resolution algorithm works as follows:

// this is pseudocode.
var inj = this;
while (inj) {
  if (inj.hasKey(requestedKey)) {
    return inj.get(requestedKey);
  } else {
    inj = inj.parent;
  }
}
throw new NoProviderError(requestedKey);

So in the following example

class Car {
  constructor(e: Engine){}
}

DI will start resolving Engine in the same injector where the Car binding is defined. It will check whether that injector has the Engine binding. If it is the case, it will return that instance. If not, the injector will ask its parent whether it has an instance of Engine. The process continues until either an instance of Engine has been found, or we have reached the root of the injector tree.

Constraints

You can put upper and lower bound constraints on a dependency. For instance, the @Self decorator tells DI to look for Engine only in the same injector where Car is defined. So it will not walk up the tree.

class Car {
  constructor(@Self() e: Engine){}
}

A more realistic example is having two bindings that have to be provided together (e.g., NgModel and NgRequiredValidator.)

The @Host decorator tells DI to look for Engine in this injector, its parent, until it reaches a host (see the section on hosts.)

class Car {
  constructor(@Host() e: Engine){}
}

The @SkipSelf decorator tells DI to look for Engine in the whole tree starting from the parent injector.

class Car {
  constructor(@SkipSelf() e: Engine){}
}

DI Does Not Walk Down

Dependency resolution only walks up the tree. The following will throw because DI will look for an instance of Engine starting from parent.

var parent = Injector.resolveAndCreate([Car]);
var child = parent.resolveAndCreateChild([
  bind(Engine).toClass(TurboEngine)
]);

parent.get(Car); // will throw NoProviderError

Bindings

You can bind to a class, a value, or a factory. It is also possible to alias existing bindings.

var inj = Injector.resolveAndCreate([
  bind(Car).toClass(Car),
  bind(Engine).toClass(Engine)
]);

var inj = Injector.resolveAndCreate([
  Car,  // syntax sugar for bind(Car).toClass(Car)
  Engine
]);

var inj = Injector.resolveAndCreate([
  bind(Car).toValue(new Car(new Engine()))
]);

var inj = Injector.resolveAndCreate([
  bind(Car).toFactory((e) => new Car(e), [Engine]),
  bind(Engine).toFactory(() => new Engine())
]);

You can bind any token.

var inj = Injector.resolveAndCreate([
  bind(Car).toFactory((e) => new Car(), ["engine!"]),
  bind("engine!").toClass(Engine)
]);

If you want to alias an existing binding, you can do so using toAlias:

var inj = Injector.resolveAndCreate([
  bind(Engine).toClass(Engine),
  bind("engine!").toAlias(Engine)
]);

which implies inj.get(Engine) === inj.get("engine!").

Note that tokens and factory functions are decoupled.

bind("some token").toFactory(someFactory);

The someFactory function does not have to know that it creates an object for some token.

Resolved Bindings

When DI receives bind(Car).toClass(Car), it needs to do a few things before it can create an instance of Car:

  • It needs to reflect on Car to create a factory function.
  • It needs to normalize the dependencies (e.g., calculate lower and upper bounds).

The result of these two operations is a ResolvedBinding.

The resolveAndCreate and resolveAndCreateChild functions resolve passed-in bindings before creating an injector. But you can resolve bindings yourself using Injector.resolve([bind(Car).toClass(Car)]). Creating an injector from pre-resolved bindings is faster, and may be needed for performance sensitive areas.

You can create an injector using a list of resolved bindings.

var listOfResolvingProviders = Injector.resolve([Provider11, Provider2]);
var inj = Injector.fromResolvedProviders(listOfResolvingProviders);
inj.createChildFromResolvedProviders(listOfResolvedProviders);

Transient Dependencies

An injector has only one instance created by each registered binding.

inj.get(MyClass) === inj.get(MyClass); //always holds

If we need a transient dependency, something that we want a new instance of every single time, we have two options.

We can create a child injector for each new instance:

var child = inj.resolveAndCreateChild([MyClass]);
child.get(MyClass);

Or we can register a factory function:

var inj = Injector.resolveAndCreate([
  bind('MyClassFactory').toFactory(dep => () => new MyClass(dep), [SomeDependency])
]);

var factory = inj.get('MyClassFactory');
var instance1 = factory(), instance2 = factory();
// Depends on the implementation of MyClass, but generally holds.
expect(instance1).not.toBe(instance2);