Friday, October 18, 2019

Why angular2 component constructor parameters must be provided

This code is not going to work unless I include SomeServiceClass in providers array. I tried input1: string. It doesn't work neither! I believe it is totally valid typescript code. The problem must be the way angular2 instantiates the component classes.
The reason you need to add SomeServiceClass to a providers array of somengModule is simple. Angular chose not to perform any dependency analysis based on code structure. They could have used the ES Module import in the injecting component to resolve the dependency and auto register it.
As evidence, Aurelia does exactly that.
In other words, in Aurelia, the following is sufficient to inject and use a dependency
import {autoinject} from 'aurelia-framework';
import {SomeServiceClass} from '...';

@autoinject export class MyComponent {
  constructor(input1: SomeServiceClass) {}
}
And it works.
How are component classed actually instantiated by Angular?
And, since I never instantiate service class, I can't imagine which service class instance is passed to component constructor.
A class in JavaScript is really a constructor function, and a constructor function is really a glorified factory function, and a factory function is really a glorified function.
What does that mean?
It means that Angular (and most DI frameworks) simply create an instance of the service when it is first requested by calling the provided class with new.
A simplified version of such code might look like
function instantiateComponent<T>(Component: new (...args: any[]) => T) {
  const requiredDependencies = resolveDependencies(Component);

  return new Component(...requiredDependencies);
}

function resolveDependencies(Dep: new (...args: any[]) => any): object[] {
  const requiredDependencies = metaDataUtils.getDependencies(Dep);

  return requiredDependencies.map(Dep => {
     const alreadyCreatedDependency = injectorCache.find(dep => dep instanceof Dep);

     if(alreadyCreatedDependency !== undefined) {
       return alreadyCreatedDependency;
     }
     else {
       const requiredDependencies = metaDataUtils.getDependencies(Dep);

       // resolution is recursive
       const newDependency = new Dep(...requiredDependencies.map(resolveDependencies));

       injectorCache.push(newDependency);

       return newDependency;
     }
  });
}
Does angular create one instance of service to be used by every component or it generates one instance for each component?
This one is tricky, by default only one instance is created per application.
This is an application level singleton and all components requesting it receive the same instance.
However, there are different implicit and explicit behaviors in Angular as well.
If a lazily loaded ngModule lists the same service in its providers array, a second instance will be created and that instance will be shared by children of the lazily loaded module. That is actually an over simplification but it is already confusing and, given that whether or not a module is loaded lazily is determined by its consumers, it can lead to serious and subtle bugs.
Finally, a component may explicitly declare its own providers in the @Component metadata. Any component doing so have will have a new instance of the service created and shared by all instances of that component.
Note: Just to be specific when you say
I noticed type of component constructor parameters must be a dependency.
you are not strictly correct. Dependencies are not types, they are values. TypeScript types are completely erased at compile time and what actually gets injected are values. A class is a value
class C {}
but TypeScript also infers the existence of some types from its declaration. But what is injected is the value C, not the type C.

No comments:

Post a Comment

Get max value for identity column without a table scan

  You can use   IDENT_CURRENT   to look up the last identity value to be inserted, e.g. IDENT_CURRENT( 'MyTable' ) However, be caut...