preface

After the previous introduction to inversion of control, I intended to write an article on dependency injection, a common pattern of inversion of control. I found a good article on Dependency injection in JavaScript. I will translate it based on my own understanding.

I like to quote that ‘programming is complexity management’. You’ve probably heard that the computer world is a giant abstraction. We simply package things and repeatedly produce new tools. Think about it for a second. The programming languages we use all include built-in functionality, which may be abstract methods based on other low-level operations, including javascript.

Sooner or later, we will need to use abstractions developed by other developers, which means we rely on other people’s code. I want to use modules without dependencies, which is obviously difficult to achieve. Even if you create a nice black box-like component, there’s always a place where all the pieces come together. This is where dependency injection comes in, and the ability to efficiently manage dependencies is urgently needed. This article summarizes the author’s views on this issue.

The target

Suppose we have two modules, a service that makes Ajax requests and a route:

var service = function() {
    return { name: 'Service' };
}
var router = function() {
    return { name: 'Router' };
}
Copy the code

Here is another function that relies on the above module:

var doSomething = function(other) {
    var s = service();
    var r = router();
};
Copy the code

To make it more interesting, the function needs to take an argument. Of course we could use the code above, but it’s not very flexible.

If we want to use ServiceXML, ServiceJSON, or we want to mock out some test module, we can’t edit the function body every time. To solve this problem, we first propose to pass the dependency as an argument to the function, as follows:

var doSomething = function(service, router, other) {
    var s = service();
    var r = router();
};
Copy the code

In this way, we pass in a concrete instance of the required module. However, here’s a new problem: imagine if the dosomething function is called in many places, and if there is a third dependency condition, we can’t change all the places where dosomething is called. For example: If we use doSomething in a lot of places:

//a.js
var a = doSomething(service,router,1)
//b.js 
var b = doSomething(service,router,2)
// If the dependency condition changes, doSomething needs a third dependency to work properly
// If the number of files is large enough, it will not be appropriate.
var doSomething = function(service, router, third,thother) {
    var s = service();
    var r = router();
    / / * * *
};

Copy the code

Therefore, we need a tool to help us manage dependencies. This is the problem the dependency injector is trying to solve. Let’s look at what we want to achieve:

  • You can register dependencies
  • An injector should take a function and return a function that has acquired the required resource
  • We shouldn’t write complex code. We need short, elegant syntax
  • The injector should preserve the scope of the function passed in
  • The function passed in should be able to accept custom arguments, not just the described dependencies.

That looks like a perfect list, so let’s try to implement it.

Requirejs/AMD

You’ve probably heard of Requirejs, which is a great dependency management solution.

define(['service'.'router'].function(service, router) {       
    // ...
});
Copy the code

The idea is to first declare the required dependencies and then start writing the functions. The order of arguments is important here. Let’s try writing a module called Injector that accepts the same syntax.

var doSomething = injector.resolve(['service'.'router'].function(service, router, other) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");
Copy the code

Here’s a quick pause to explain doSomething’s function body, using expect. Js as an assertion library to make sure my code works as expected. A little bit of TDD (Test Driven Development).

The following is the beginning of our Injector module, a singleton was a good choice so it worked well in different parts of our application.

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {}}Copy the code

From the point of view of the code, it is indeed a very simple object. There are two functions and a variable that acts as a storage queue. All we need to do is check the DEPS dependencies array and look for the answer in the Dependencies queue. All that is left is to call the.apply method to concatenate the parameters passed to the function.

 // The dependency is passed to func as an argument after processing
resolve: function(deps, func, scope) {
    var args = [];
    If there is no dependency module in the dependency queue, the dependency cannot be called, so an error is reported.
    for(var i=0; i<deps.length, d=deps[i]; i++) {
        if(this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can\'t resolve '+ d); }}// Process the arguments, concatenating the arguments after the dependencies to correspond to the positions of the arguments in the function
    return function() {
        func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments.0))); }}Copy the code

If scope exists, it can be passed efficiently. Array. The prototype. Slice. The call (the arguments, 0) to the arguments (Array) into a true Array. So far it looks good. It can pass the test. The current problem is that we have to write the required dependencies twice, in an immutable order, and the extra arguments only last.

Reflection to realize

From Wikipedia, reflection is the ability of a program to examine and modify the structure and behavior of an object at run time. In short, in the context of JS, it means to read and analyze the source code of an object or function. Take a look at doSomething at the beginning. If you use dosomething.tostring (), you get the following result.

"function (service, router, other) {
    var s = service();
    var r = router();
}"
Copy the code

This way of converting a function to a string gives us the ability to get the desired parameters. And more importantly, their name. Here’s how Angular implements dependency injection. I got a few regular expressions from Angular to get arguments:

/^function\s*[^\(]*\(\s*([^\)]*)\)/m
Copy the code

So we can change the resolve method:

tip

So here, I’m going to show you the test example that should make a little bit more sense.

var doSomething = injector.resolve(function(service, other, router) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");
Copy the code

Let’s move on to our implementation.

resolve: function() {
    // An array of parameters passed by AGRS to func, including dependent modules and custom parameters
    var func, deps, scope, args = [], self = this;
    // Get the func passed in, mainly for the following split string
    func = arguments[0];
    // get the array of dependent modules
    deps = func.toString().match(/^functions*[^(]*(s*([^)]*))/m) [1].replace(/ /g.' ').split(', ');
    // The scope to bind is not specified
    scope = arguments[1) | | {};return function() {
        // Convert arguments to an array
        // when called again, doSomething("Other");
        // Other is a to complement the missing module.
        var a = Array.prototype.slice.call(arguments.0);
        // Loop dependent module array
        for(var i=0; i<deps.length; i++) {
            var d = deps[i];
            // If the module in the dependency queue exists and is not empty, push it into the parameter array.
            // If there is no module in the queue, take the first element from a and push it.args.push(self.dependencies[d] && d ! =' ' ? self.dependencies[d] : a.shift());
        }
        // Dependencies are passed in as argumentsfunc.apply(scope || {}, args); }}Copy the code

When we use this re to process functions, we get the following result:

["function (service, router, other)"."service, router, other"]
Copy the code

All we need is the second item, and once we clear the array and split the string, we’ll get the dependent array. The main changes are as follows:

var a = Array.prototype.slice.call(arguments.0); . args.push(self.dependencies[d] && d ! =' ' ? self.dependencies[d] : a.shift());
Copy the code

This way we loop through the dependencies, and if something is missing we can try to get it from the Arguments object. Fortunately, shift only returns undefined when the array is empty instead of throwing an error. So here’s how the new version works:

// There is no need to declare dependency modules before
var doSomething = injector.resolve(function(service, other, router) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");
Copy the code

So you don’t have to repeat it, you can change the order. We’ve replicated the magic of Angular. However, this is not perfect, and compression can break our logic, which is a big problem with reflection injection. Because compression changes the names of parameters, we have no ability to resolve these dependencies. Such as:

// There is a problem with matching by key
var doSomething=function(e,t,n){var r=e();var i=t()}
Copy the code

The Angular team’s solution is as follows:

var doSomething = injector.resolve(['service'.'router'.function(service, router) {}]);Copy the code

It looks exactly the same as the require.js we started with. The author personally cannot find a better solution in order to accommodate both approaches. The final plan looks like this:

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function() {
        var func, deps, scope, args = [], self = this;
        // This situation is compatible form, first declared
        if(typeof arguments[0= = ='string') {
            func = arguments[1];
            deps = arguments[0].replace(/ /g.' ').split(', ');
            scope = arguments[2) | | {}; }else {
            // The first way of reflection
            func = arguments[0];
            deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m) [1].replace(/ /g.' ').split(', ');
            scope = arguments[1) | | {}; }return function() {
            var a = Array.prototype.slice.call(arguments.0);
            for(var i=0; i<deps.length; i++) {
                vard = deps[i]; args.push(self.dependencies[d] && d ! =' '? self.dependencies[d] : a.shift()); } func.apply(scope || {}, args); }}}Copy the code

Resolve now accepts two or three arguments, if two is the first one we wrote, and if three, the first argument will be parsed and populated into deps. Here is an example (I always thought it would be easier to read if I put this example first). :

// A module other is missing
var doSomething = injector.resolve('router,,service'.function(a, b, c) {
    expect(a().name).to.be('Router');
    expect(b).to.be('Other');
    expect(c().name).to.be('Service');
});
// The Other passed here will be used to piece together
doSomething("Other");
Copy the code

Argumets [0] argumets[0] argumets[0] argumets[0] argumets[0] argumets[0] argumets[0] argumets[0] argumets[0] argumets[0] argumets

Inject directly into the scope

Sometimes, we use the third injection method, which involves scoped operations (or by another name, this object), and is not often used

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {
        var args = [];
        scope = scope || {};
        for(var i=0; i<deps.length, d=deps[i]; i++) {
            if(this.dependencies[d]) {
                // Add the dependency to scope directly
                // This can be called directly from the function scope
                scope[d] = this.dependencies[d];
            } else {
                throw new Error('Can\'t resolve '+ d); }}return function() {
            func.apply(scope || {}, Array.prototype.slice.call(arguments.0)); }}}Copy the code

So what we’re doing is we’re adding dependencies to the scope, and the nice thing about that is we don’t have to add dependencies to the parameters, they’re already part of the scope of the function.

var doSomething = injector.resolve(['service'.'router'].function(other) {
    expect(this.service().name).to.be('Service');
    expect(this.router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");
Copy the code

conclusion

Dependency injection is one of those things we all do, perhaps without realizing it. Even if you haven’t, you’ve probably used it many times. Through this article, I have deepened my understanding of this familiar and strange concept, hoping to help students in need. Finally, limited personal ability, translation errors are welcome to point out, common progress. Thanks again to the original author of the original address. For more articles please visit my blog