AngularJS依赖注入背后的秘密
发布在用Angular开发web应用2014年3月2日view:11555
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

如果你曾经使用过AngularJS创建一些东西,那你一定知道你经常忽视了其中的一些”魔法”,因为它们会自动运行。对于我来说最不可思议的部分就是依赖注入。你只需要在你的控制器函数中添加一个变量,你立刻就能够访问一个强大的Angular service。这非常不可思议而你一直否非常信任它 – 知道出现了问题。

事实证明想要破坏你的AngularJS应用的一个最简单的方式就是精简你的javaScript代码。当我想要把应用部署到生产环境中时这个问题发生了。当时,我的Angular应用放置在一个Rails应用中,然后Rails就自动精简了JavaScript代码。后来我发现Angular的教程中的一篇文章教你应该“将变量写成数组的形式”来解决这个问题,但是还是没有说清楚究竟发生了什么。

在下面的文章中,我们将做的事情有:

  1. 创建一个简单的AngularJS应用
  2. 看看依赖注入究竟有多神奇
  3. 调查依赖注入书如何在AngularJS中实现的
  4. 通过精简JavaScript来破坏我们的应用
  5. 理解弥补措施是怎样运行的

一个使用GitHub API的AngularJS应用

我们现在来使用Github的API来创建一个简单的AngularJS应用,它用来找出angular.js项目中最新的提交。

下面是这个应用的源码:

<html ng-app>
<head>
    <script src='http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.js'></script>   

    <script>
    var MyController = function($scope,$http){
        $http.get('https://api.github.com/repos/angular.js/commits')
             .success(function(commits){
                $scope.commits = commits;
             })
    }
    </script>  

    <body ng-controller='MyController'>
      <ul>
        <li ng-controller='commit in commits'>
          {{commit.commit.committer.date | date}}
          <a ng-herf="https://github.com/angular/angular.js/commit/{{commit.sha}}">{{commit.sha}}</a>  
          {{commit.commit.message}}
            </li>
        </ul>
  </body>
</html>   

我们来回顾一下上面的代码:

  • 第1行 – ng-app属性告诉angular这个页面是一个angular应用
  • 第6 - 11行 – 一些JavaScript代码定义控制器并告诉angular在注入$scope和$http services
  • 第15行 – ng-controller="MyController"属性告诉angular body标签将由MyController控制器来进行作用域控制
  • 第18 - 22行 – 这部分将在DOM中扩展出许多li元素,每一条都包含一次commit信息

这段代码非常酷!当然并非人人都是angular的粉丝,在《Dependency injection is not a virtue》一文中,DHH争论说依赖注入是JAVA语言中一个老旧的特性,它在Ruby中完全没有必要存在。AngularJS指出url的哈希部分并自动将它插入到页面中。

神秘的依赖注入

那么依赖注入位于代码的什么地方呢?现在我们来将控制器中的函数重新排列一下顺序:

var MyController = function($http, $scope) {
  $http.get('https://api.github.com/repos/angular/angular.js/commits')
    .success(function(commits) {
      $scope.commits = commits
    })
}   

我们将function($scope,$http)改成了function($http,$scope),令人吃惊的是程序任然正常运行!

在AngularJS内部代码是这样运行的:

  1. 它从每一个参数的位置知道我们的控制器需要哪些service(原来是$scope第一$http第二,现在相反);
  2. 它决定什么对象应该“提供”每一个命名的service(例如,$httpProvider提供$http);
  3. 用合适的provider来调用我们的控制器(无论是MyController(scope,$httpProvider)还是MyController($httpProvider,scope)

AngularJS是怎样完成步骤1的呢?

在JavaScript中,参数的顺序很重要,但是参数的名字对于调用的函数来说不重要。

我们现在来看看一些直观的JavaScript代码来确认这点。如果我们定义了一个函数divide来接受两个参数:

var divide = function(numerator,denominator){
    return numerator / denominator;
}

正如我们期望的 divide(1 , 2) == 0.5

当我们改变了参数的顺序时,我们也改变了函数的定义:

var divide = function(denominator,numerator){
    return numerator / denominator;
}  

现在根据定义divide(1 , 2) == 2

现在看来,AngularJS似乎是超出了JavaScript所支持的范围。

依赖注入如何在JavaScript中实现命名参数

我们看到了AngularJS的依赖注入依赖于参数的名字而不是它们的顺序,这个特性叫做命名参数,它本身并不存在与JavaScript中。AngularJS是怎么实现它的呢?

AngularJS很聪明的使用了每一个JavaScript中的对象都拥有一个toString函数的事实,它在控制器决定应该如何被调用之前解析并提取出参数的名字。

现在我们来看看toString函数来看看它是怎么工作的。在函数对象中它返回了定义该对象的源代码,包括带有参数名称的函数签名。

当我们在我们的divide函数中调用它时我们可以看到它的行为,divide.toString() == "function(numerator,denominator){return numerator / denominator;}"

AngularJS将上述步骤放在injector.js内一个叫做annotate的函数中,这个函数接收一个函数并返回它的参数的名字:

$inject = [];
fnText = fn.toString().replace(SCRIP_COMMENTS,'');
argDecl = fnText.math(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT),function(arg){
    arg.replace(FN_ARG,function(all,underscore,name){
        $inject.push(name);
    });
});
fn.$inject = $inject;
//...
return $inject;   
  • 第2行 – 使用toString()来得到函数的定义
  • 第3行 – 使用AngularJS的模式匹配FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m来找出函数签名
  • 第4 - 8行 – 循环所有参数并将它们的名字存储在一个数组中
  • 第11行 – 返回包含参数名字的数组

我们可以看到divide函数中的行为,angular.injector().annotate(divide) == ["numerator","denominator"]

用精简来破坏应用

这个使用toString的技巧有一个大问题。我们经常在将JavaScript发送的浏览器之前将它进行压缩,在上面的情形中这段JS代码会被自动压缩精简。

var MyController = function(e,t) {
  t.get("https://api.github.com/repos/angular/angular.js/commits")
    .success(function(t) {
      e.commits=t
    })
}   

将参数名称精简为e或者t之后,AngularJS无法将它映射到相应的service名字上。它怎么知道t代表$httpProvider或者e代表$scope?

AngularJS的annotate函数非常的复杂,以致于它能够处理这种精简压缩的情况。事实上,我们在前面只是看到了这个函数定义的一部分。当我们查看完整的函数定义时,我们可以看到它可以表明一个函数或者一个参数数组。在数组的形式下,第一个字符串名称是第一个参数,第二个字符串名称是第二个参数,等等。此时,参数的真实名称并不重要。

function annotate(fn) {
  var $inject,
      fnText,
      argDecl,
      last;

  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      // 在此省略了我们前面看到的代码
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn')
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;

当我们重新定义我们的MyController函数时:

var MyController = ['$scope', '$http', function($scope, $http) {
  $http.get('https://api.github.com/repos/angular/angular.js/commits')
    .success(function(commits) {
      $scope.commits = commits
    })
}]

在精简时,字符串并不会被改变因此annotate函数依然正常运行:

var MyController= ["$scope", "$http", function(e,t) {
  t.get("https://api.github.com/repos/angular/angular.js/commits")
    .success(function(t) {
      e.commits=t
    })
}]  

我们可以看到annotate函数解析了数组angular.injector().annotate(MyController) == ["$scope", "$http"]

此时,压缩后的程序也能正常运行。

如果你坚持看到这里,我希望你能够从本文中了解到AngularJS中的依赖注入究竟是怎么一回事。其实你所需要了解的仅仅是使用一个数组,数组的最后一个元素是一个函数。希望你能在本文中有所收获。


本文译自The “Magic” behind AngularJS Dependency Injection,原文地址http://www.alexrothenberg.com/2013/02/11/the-magic-behind-angularjs-dependency-injection.html

如果你觉得本文对你有帮助,请点击为我提供赞助

评论
发表评论
2年前
添加了一枚【评注】:此处应该是 ng-repeat吧
4年前

终于找到答案了,参数顺序的问题疑惑了我好久,每次写下一个controller时都被参数顺序问题纠结到死。。。

WRITTEN BY
张小俊128
Intern in Baidu mobile search department。认真工作,努力钻研,期待未来更多可能。
TA的新浪微博
PUBLISHED IN
用Angular开发web应用

讲述Angular开发框架的基础知识,帮助读者快速学习并深入理解Angular的开发理念和具体应用方式

我的收藏