Home >Web Front-end >JS Tutorial >In-depth understanding of dependency injection in Javascript_javascript skills

In-depth understanding of dependency injection in Javascript_javascript skills

WBOY
WBOYOriginal
2016-05-16 16:55:02928browse


Sooner or later you need to use other developers' abstractions - that is, you rely on other people's code. I'd love dependency-free (dependency-free) modules, but that's hard to achieve. Even those nice black box components you create rely on something more or less. This is where dependency injection comes into play. The ability to effectively manage dependencies is now an absolute necessity. This article summarizes my exploration of the problem and some of its solutions.

1. Goal
Imagine we have two modules. The first one is responsible for Ajax request service (service), and the second one is routing (router).

Copy code The code is as follows:

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

We have another function to use to these two modules.
Copy code The code is as follows:

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

To make it look more interesting, this function accepts one parameter. Of course, we can completely use the above code, but it is obviously not flexible enough. What if we want to use ServiceXML or ServiceJSON, or what if we need some test modules. We can't solve the problem just by editing the function body. First, we can resolve dependencies through the parameters of the function. That is:
Copy code The code is as follows:

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

We achieve what we want by passing additional parameters, however, this Will bring new problems. Imagine if our doSomething methods were scattered throughout our code. If we need to change dependencies, we cannot change all the files that call the function.

We need a tool that can help us do this. This is the problem that dependency injection tries to solve. Let’s write down some goals our dependency injection solution should achieve:

We should be able to register dependencies
1. Injection should take a function and return a function we need
2. We can’t write too much - we need to streamline and beautiful syntax
3 .Injection should maintain the scope of the passed function
4. The passed function should be able to accept custom parameters, not just dependency descriptions
5. It’s a perfect checklist, let’s implement it.
3. RequireJS/AMD method
You may have heard of RequireJS, it is a good choice to solve dependency injection.

Copy code The code is as follows:

define(['service', 'router'], function(service, router) {                                                    The order of parameters here is important. As mentioned above, let's write a module called injector that accepts the same syntax.



Copy code
The code is as follows: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");
I should before continuing To explain clearly the content of the doSomething function body, I use expect.js (the assertion library) just to ensure that the code I write behaves the same as I expect, reflecting a little TDD (test-driven development) method.
Let’s start with our injector module. This is a great singleton pattern, so it can work well in different parts of our program.




Copy code
The code is as follows:

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

}
}


This is a very simple object with two methods, one for storing properties. What we do is check the deps array and search for the answer in the dependencies variable. All that's left is to call the .apply method and pass the arguments from the previous func method.
Copy code The code is as follows:

resolve: function(deps, func, scope) {
var args = [];
for(var i=0; i if(this.dependencies[d]) {
args.push (this.Dependencies [d]);
} else {
Throw New Error ('Canon't Resolve' d);
func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
}
}

scope is optional Yes, Array.prototype.slice.call(arguments, 0) is required to convert the arguments variable into a real array. So far so good. Our test passed. The problem with this implementation is that we need to write the required parts twice, and we cannot mix up their order. Additional custom parameters always come after dependencies.
4. Reflection method
According to the definition of Wikipedia, reflection refers to the ability of a program to inspect and modify the structure and behavior of an object at runtime. Simply put, in the context of JavaScript, this specifically refers to reading and analyzing the source code of an object or function. Let's complete the doSomething function mentioned at the beginning of the article. If you output doSomething.tostring() in the console. You will get the following string:



Copy the code The code is as follows:"function (service , router, other) {
var s = service();
var r = router();
}"

The string returned by this method gives us the ability to traverse the parameters , and more importantly, being able to get their names. This is actually how Angular implements its dependency injection. I was a little lazy and directly intercepted the regular expression for obtaining parameters in the Angular code.


Copy code The code is as follows:/^functions*[^(]*(s* ([^)]*))/m
We can modify the resolve code as follows:


Copy code The code is 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 var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies [d]: a.shift());
}
func.apply(scope || {}, args);
}
}

We execute the regular expression The results of other)", "service, router, other"]

Looks like we only need the second item. Once we clear the spaces and split the string we get the deps array. There is only one big change:
Copy the code The code is as follows:

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

We loop through the dependencies array and try to get it from the arguments object if a missing item is found. Thankfully, when the array is empty, the shift method just returns undefined instead of throwing an error (thanks to the idea of ​​​​the web). The new version of injector can be used as follows:
Copy the code The code is as follows:

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");

There is no need to rewrite dependencies and they The order can be disrupted. It still works and we successfully replicated Angular's magic.

However, this approach is not perfect, which is a very big problem with reflex type injections. Compression will break our logic because it changes the parameter names and we won't be able to maintain the correct mapping. For example, doSometing() might look like this when compressed:

Copy the code The code is as follows:

var doSomething=function(e,t,n){var r=e();var i=t()}
The solution proposed by the Angular team looks like:

var doSomething = injector.resolve(['service', 'router', function(service, router) {

}]);


This looks a lot like the solution we started with. I couldn't find a better solution so decided to combine the two approaches. Below is the final version of the injector.
Copy code The code is as follows:

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]                                                                                                                                                                             i=0; i               var d = deps[i]; : a.shift());
                                                                        
The resolve visitor accepts two or three parameters. If there are two parameters, it is actually the same as written earlier in the article. However, if there are three parameters, it will convert the first parameter and fill the deps array, here is a test example:
Copy code The code is as follows:

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');
} );
doSomething("Other");

You may notice that there are two commas after the first parameter - note that this is not a typo. The null value actually represents the "Other" parameter (placeholder). This shows how we control the order of parameters.

5. Directly inject Scope
Sometimes I use the third injection variable, which involves the scope of the operating function (in other words, the this object). Therefore, many times there is no need to use this variable.

Copy code The code is as follows:

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 if(this.dependencies[d] ) {
                scope[d] = this.dependencies[d];                              }
return function() {
                 func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));                                                   All we do is add dependencies to the scope. The advantage of this is that the developer no longer has to write dependency parameters; they are already part of the function scope.




Copy code

The code is as follows:

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");
6. Conclusion
In fact, most of us have used dependency injection, but we didn’t realize it. Even if you don't know the term, you've probably used it a million times in your code. I hope this article can deepen your understanding of it.
Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn