To understand dependency injection, you need to know where the concept came from.

As we all know, javascript, as an object-oriented language, is essentially a combination of function and prototype. What we usually call this refers to the trajectory of function, and its object-oriented encapsulation, polymorphism and inheritance are realized on the basis of prototype.

Es6 gives javascript the class attribute, which we know is just syntactic sugar for functions, but it does implement classes in the traditional sense, and thus allows class features to be applied. Class can implement dependency injection, also can be applied in our code implementation.

On the basis of class, we also implement dependency injection through decorator. Let’s take a quick look at the basics.

I. Introduction to the basic knowledge of dependency injection

1.1 the Decorator

1.1.1 Object. DefineProperty ()

Syntax: Object.defineProperty(obj, prop, Descriptor)

parameter

  • obj

    The object for which attributes are to be defined.

  • prop

    The name or Symbol of the property to be defined or modified.

  • descriptor

    The property descriptor to define or modify.

A property descriptor is an object with the following properties that can be modified.

  • configurable

    The descriptor of this property can be changed and deleted from the corresponding object only when the property’s key is set to true. The default is false.

  • enumerable

    The property appears in the object’s enumerable property if and only if its Enumerable key value is true. The default is false.

  • value

    The value corresponding to this property. It can be any valid JavaScript value (numeric value, object, function, etc.). The default is undefined.

  • writable

    The value of the property, the value above, can be changed by the assignment operator (en-us) if and only if the writable key of the property is true. The default is false.

  • get

    Property, or undefined if there is no getter. This function is called when the property is accessed. No arguments are passed, but this object is passed (because of inheritance, this is not necessarily the object that defines the property). The return value of this function is used as the value of the property. ** defaults to undefined **.

  • set

    Property, or undefined if there is no setter. This function is called when the property value is modified. This method takes an argument (that is, the new value being assigned) and passes in the this object at the time of assignment. ** defaults to undefined **.

A Decorator is nothing more than a syntactic sugar that takes advantage of ES5’s Object.defineProperty, which is essentially a generic function that extends class attributes and class methods. It receives three parameters (target, name, descriptor), which have the same meaning as Object.defineProperty.

Let’s use specific code to demonstrate:

@eat
class Pig {
  constructor(){}}function eat(target, key, descriptor) {
  console.log('eat');
  console.log(target);
  console.log(key);
  console.log(descriptor);
  target.prototype.eat = 'Eat eat eat';
}

const peppa = new Pig();
console.log(peppa.eat);

/ / to eat
// [Function: Pig]
// undefined
// undefined
/ / to eat eat

Copy the code

We declare a Pig class and then we declare a decorator function called eat. We print out the three arguments to eat and add a property called eat to the prototype of the first argument, target, and set it to ‘eat eat eat’. We then decorate the function eat on the Person class itself. Finally, construct an instance of Pig, peppa, and print the EAT property on the PEPPA.

Then we can see from the following result that the code prints’ eat ‘first, then the parameter target, then the parameter key, then the parameter descriptor, and finally the peppa’s eat property. This is because the decorator changes the behavior of the class at compile time, not run time. This means that the decorator can run the code at compile time. That is, decorators are essentially functions that are executed at compile time.

To sum up, decorators have the following characteristics:

  • Simple to use and easy to understand
  • Extend class attributes and class methods without changing the original code
  • Is a function that is executed at compile time

1.1.2 Scope of decorators

Decorators work on classes and their properties, not on functions.

1.1.2.1 Applies to properties

Take a logger as an example:

class Math {
  @log
  add(a, b) {
    returna + b; }}function log(target, name, descriptor) {
  var originalMethod = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling "${name}" with`.arguments);
    return originalMethod.apply(null.arguments);
  };

  return descriptor;
}

const math = new Math(a);// The parameters passed will be printed
math.add(2.4); // Calling "add" with 2, 4
Copy the code

1.1.2.1 Applies to classes

function Car(brand, price) {
  return function(target) {
    target.brand = brand;
    target.price = price;
  }
}

@Car('benz'.'1000000')
class Benz(a){ }

@Car('BMW'.'600000')
class BMW(a){ }

@Car('audi'.'400000')
class Audi(a){}Copy the code

1.2 Reflect the Metadata

Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declaration time. It can be used for classes, class members, and parameters.

Simply put, you can use decorators to add custom information to a class. This information is then extracted by reflection. Of course, you can also add this information through reflection.

TypeScript already supports TypeScript in version 1.5+. You just need to:

  • npm i reflect-metadata --save.
  • intsconfig.jsonIn the configurationemitDecoratorMetadataOptions.
  • Introduced in the first line of code'relfect-metadata'
/ * * *@param MetadataKey Key * of the metadata entry@param Value * of metadataValue metadata entry@returns The decorator function */
function metadata(metadataKey: any, metadataValue: any) :{
    (target: Function) :void;
    (target: Object.propertyKey: string | symbol): void;
};
Copy the code

Reflect.metadata is used as a Decorator to add metadata to a class when it decorates a class, and to add metadata to a class stereotype when it decorates a class attribute, such as:

@Reflect.metadata('inClass'.'A')
class Test {
  @Reflect.metadata('inMethod'.'B')
  public hello(): string {
    return 'hello world'; }}console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod'.new Test(), 'hello')); // 'B'
Copy the code

Then we need to implement our own decorator and design one of the keys using the available reflection metadata. Only three are currently available:

  • Type metadataUse metadata keys"design:type".
  • Parameter type metadataUsing the metadata key"design:paramtypes".
  • Returns type metadataUse metadata keys"design:returntype".

What is dependency injection

As we all know, javascript is an object-oriented programming (OOP) language, dependency injection (DI) and inversion of control (IoC) are concrete methods, which are the embodiment of the dependency inversion principle in OOP theory. Reduce coupling between objects through information hiding.

The process of transferring the task of creating an object to another class and using the dependency directly is called dependency injection. (DI)

IOC (Inversion of Control) is a container that can automatically instantiate concrete classes and manage relationships between objects. With this automated container, we will not focus on concrete relationships, but only on relationships between abstractions, and can eliminate manual instantiation.

When the requirements reach a certain degree of complexity, we can’t start from cloth and buttons in order to put on clothes. It’s better to encapsulate some operations and put them into a factory or warehouse. What we need can be directly taken from the warehouse of the factory.

This is where dependency injection comes in. We implement an IOC container (repository) and then take the clothes from the repository and give them to wear as properties.

IOC is a good idea for decoupling. In development, IOC means that you design objects to be controlled by the container, rather than the traditional way of controlling them directly from within.

The comparison shows that the modules that use dependency injection are:

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats'; }}Copy the code

(This code is taken from nest’s official website)

Or something like this:

import { Component } from '@angular/core';

@Component({
  selector: 'hello-world'.template: ` 

Hello World

This is my first component!

`
,})export class HelloWorldComponent { // The code in this class drives the component's behavior. } Copy the code

(Excerpt from Angular’s official website)

Iii. Simple DI implementation

An IoC container Injector has been implemented and a root container rootInjector has been instantiated.

Injectable(…) (Used to inject individual dependent classes into the root container)

3, Implement annotation-based attribute injection method Inject(…) (Insert the dependency required by the class from the root container into the class, or create the dependency if the root container does not exist.)

import 'reflect-metadata';

// All kinds of operations in the factory
export class Injector {
  private readonly providerMap: Map<any, any> = new Map(a); private readonly instanceMap:Map<any, any> = new Map(a); public setProvider(key: any,value: any): void {
    if (!this.providerMap.has(key)) {
      this.providerMap.set(key, value);
    }
  }
  
  public getProvider(key: any): any {
    return this.providerMap.get(key);
  }

  public setInstance(key: any, value: any): void {
    if (!this.instanceMap.has(key)) {
    	this.instanceMap.set(key, value);
		}
  }
  
  public getInstance(key: any): any {
    if (this.instanceMap.has(key)) {
      return this.instanceMap.get(key);
    }
    return null; }}// represents the root injector (used to hold the root container for each dependency)
export const rootInjector = new Injector();

The class decorator returns a value that replaces the declaration of the original class with the supplied constructor
export function Injectable() : (_constructor: any) = >any {
  return function (_constructor: any) :any {
    rootInjector.setProvider(_constructor, _constructor);
    return _constructor;
  };
}

// Inject dependencies into producers
export function Inject() : (_constructor: any, propertyName: string) = >any {
  return function (_constructor: any, propertyName: string) :any {
    GetMetadata ('design:type') gets the type of the property and uses it as a unique identifier to get the instance of ** Injector.getInstance. If so, the attribute is mapped directly to the found instance. This ensures that we get a singleton every time we use the property of the ** decorator. * /
    const propertyType: any = Reflect.getMetadata('design:type', _constructor, propertyName);
    const injector: Injector = rootInjector;
    let providerInsntance = injector.getInstance(propertyType);
    if(! providerInsntance) {const providerClass = injector.getProvider(propertyType);
      providerInsntance = new providerClass();
      injector.setInstance(propertyType, providerInsntance);
    }
    _constructor[propertyName] = providerInsntance;
  };
}

@Injectable()
class Cloth {
  name: string = 'linen';
}

@Injectable()
class clothes {
  // By injecting class Cloth into class Clothes, class Clothes have the ability to use class Cloth
  @Inject()
  cloth: Cloth;
  clotheName: string;
  constructor() {
    this.cloth = this.cloth;
    this.clotheName = this.clotheName;
  }
  updateName(name: string) {
    this.clotheName = name; }}class Humanity {
  @Inject() 
  clothes: Clothes;
  name: string;
  constructor(name: string) {
    this.clothes = this.clothes;
    this.name = name;
  }
  update(name: string) {
    this.clothes.updateName(name); }}// Singleton: for data state maintenance (one change all changes)
const people = new Humanity('syz');
console.log(people);
// Humanity {
{cloth: cloth {name: 'cloth'}, clotheName: undefined}
// }

people.update('Nike');
console.log(people);
// Humanity {
// clothes: clothes {cloth: cloth {name: 'cloth'}, clotheName: 'Nike'}
// }
Copy the code

(This section is from juejin.cn/post/684490…)

DI can also be combined with Rxjs for reactive programming, which I won’t expand on here.

Four or more

What is IOC(Inversion of Control), DI(Dependency Injection)

In-depth understanding of JavaScript prototypes

A dozen lines of code implement a TS dependency injection

Reflect Metadata

JavaScript Reflect Metadata details

Typescript: reflect metadata api

React New paradigm – DI, RxJS & Hooks