As a developer, you will inevitably have to use modules provided by other developers. I personally don’t like relying on third-party modules, but it’s hard to do. Even if you have very well-packaged components, you still need something that fits together perfectly. That’s what dependency injection does. The ability to manage dependencies effectively is now an absolute necessity. This article summarizes my views on the problem and some solutions.

Let’s look at an example

Suppose you have two modules: Service (a service that implements Ajax calls) and Router (a module that implements routing control).

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

There is now a place to rely on the above two modules:

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

To make the example a little more interesting, we pass an argument to the doSomething function. The code above does everything you need, but it lacks some flexibility. Imagine if we wanted to use ServiceXML or ServiceJSON? Instead of changing the function every time, the first thing to think about is passing the dependency as an argument to the function:

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

We just need to pass in the specific module we want (such as ServiceXML) each time a function is called. But this raises a new question: what if we need to add a third dependency?

What we need is a tool that can do that for us. This is the problem that dependency injectors solve.

What a dependency injection solution should achieve:

  1. Register dependencies;
  2. Take a function and return a function that somehow gets the required resource;
  3. Keep the syntax simple;
  4. Ability to preserve function scope;
  5. Can support custom parameters;

RequireJS / AMD

You’ve probably heard of RequireJS. It provides a good idea:

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

Its main principle is to first describe the required dependencies and then write the functions. The order of arguments is important here. Let’s say we wrote a module called Injector, implemented with 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 I use the Expect assertion library to make sure I’m writing code that behaves as I expect it to, a TDD test.

Now how does injector work? To ensure that it is called correctly throughout the application, it is designed in singleton mode:

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

    }
}
Copy the code

The Injector object is very simple: two functions and one object (dependencies store dependencies). What we do is check the DEPS array and look for the dependent module in the Dependencies variable. The rest just calls the.apply method for the previous func argument.

resolve: function(deps, func, scope) { var args = []; 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); } } return function() { func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0))); }}Copy the code

Array. The prototype. Slice. The call (the arguments, 0) convert the arguments into a true Array. Run our code, the test case can run normally, indicating that the test passed.

The problem with this version is that the order of module dependencies is constant, and the additional argument we add, other, tends to come last.

Introduce Reflection: Reflection

Reflection, the ability of a program to examine and modify the structure and behavior of an object at runtime. Simply put, in the context of JavaScript, it means reading the source code of an object or function and analyzing it.

Let’s go back to the definition of the doSomething function at the beginning of this article. Console.log (dosomething.tostring ()) gives the following information:

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

Through this method, we gain the ability to get the expected parameters of the function. The most important thing is that we get the name of the parameter, which is what Angular famously does. How to get parameters is a reference to Angular’s internal regular expressions:

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

Change the resolve method as follows:

resolve: function() {
    var func, deps, scope, args = [], self = this;
    func = arguments[0];
    deps = func.toString().match(/^functions*[^(]*(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++) {
            var d = deps[i];
            args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
        }
        func.apply(scope || {}, args);
    }        
}
Copy the code

Using the regular expression, we extract the result of doSomething:

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

All we care about is the second entry in this array, which by replacing whitespace, string splitting, we get the DEPT array.

The next step is very simple:

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

Look for corresponding dependencies through Dependencies, and use arguments objects if none are found. The advantage of using shift is that even if our array is empty, we return undefined instead of throwing an exception.

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

We found that the code was much leaner and, most importantly, the order of arguments could be changed flexibly. We replicate Angular’s capabilities.

Then, when we were ready to release this code to production, we found a serious problem: the code before going live was usually compressed, and changing the parameter names affected the dependency handling of the program. For example, our doSomething will be processed as:

var doSomething=function(e,t,n){var r=e(); var i=t()}Copy the code

Angular’s solution is:

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

Very much like the way the article started. Ultimately, we need to combine two approaches:

var injector = { dependencies: {}, register: function(key, value) { this.dependencies[key] = value; }, resolve: function() { var func, deps, scope, args = [], self = this; if(typeof arguments[0] === 'string') { func = arguments[1]; deps = arguments[0].replace(/ /g, '').split(','); scope = arguments[2] || {}; } else { func = arguments[0]; deps = func.toString().match(/^functions*[^(]*(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++) { var d = deps[i]; args.push(self.dependencies[d] && d ! = ' '? self.dependencies[d] : a.shift()); } func.apply(scope || {}, args); }}}Copy the code

Injection scope

Sometimes I use an injection of the third variant. It involves the scope of the operation function. So, it doesn’t work in most cases, so I’ll separate it out.

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]) { 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

All we need to do is inject all dependencies into the scope of the executing function. The advantage of this is that the dependency does not need to be passed as a parameter:

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

The end of the

Dependency injection is something we do all the time, but probably never really think about. Even if you don’t know this word yet, you’ve probably used it countless times in your project. Hopefully this article will give you a better understanding of how it works.