首页 > 网站 > WEB开发 > 正文

【原创】angularjs1.3.0源码解析之directive

2024-04-27 14:16:19
字体:
来源:转载
供稿:网友

【原创】angularjs1.3.0源码解析之directive

# Angular指令编译原理

前言

angular之所以使用起来很方便,是因为通常我们只需要在html里面引入一个或多个(自定义或内置的)指令就可以完成一个特定的功能(这也是angular推荐的方式),比如:一个简单的双向绑定(用ng-model指令),或者模板循环渲染(用ng-repeat指令),又或者是模板是否显示(用ng-if指令),而对于这些指令的内部实现一般我们无需太多关心(除非你想深入了解),我们更乐意于把侧重点放在指令如何使用(即API)上。

我们知道一个应用是由多个功能组成的,而在angular应用中,一个功能又是由一个或多个指令所完成,那么我们可以认为angular应用被很多指令所驱动着,当我们给angular应用添加了所有我们需要的指令后,angular内部则会负责帮我们编译和运行所有指令,从而完成特定功能的实现。

那么,对于指令,我们自然会有以下的疑问:

  1. 指令是什么?
  2. 指令如何定义存储?
  3. 指令如何编译运行?

带着疑问,我们慢慢一步一步深入到源码compile.js中。

指令

指令从html的角度,可以认为指令名字是一个标识符,可以作为元素名(E),元素属性(A),注释(M),类名(C)出现在html中;而从javascript的角度,则可以认为是返回的一个规范化的有特殊意义的指令对象。

听起来有点抽象,简而言之一切都得从自定义一个简单指令开始:

html

<body ng-app="myApp">  <div my-directive></div></body>

js

angular.module('myApp', [])  .directive('myDirective', function() {    // 返回一个对象(暂且称之为指令对象)    return {      restrict: 'A',      replace: true,      scope: true,      template: '<span>hello world</span>',      compile: function (tElement) {        console.log('complile: ', tElement);        return function (scope, elem) {          console.log('link: ', elem);        }      }    }  });

上面的代码,大家肯定都很熟悉,就是利用定义好的模块对象myApp调用它的directive()方法,成功注册了一个名字叫做myDirective的指令,然后在html中便可以将my-directive作为元素属性的方式调用这个指令(这里只是简单的模板替换和打印日志)。

这里其实我们关注的其实有两点:

  1. directive方法的实现
  2. 返回的指令对象(暂且这样称呼)每个字段的含义

指令定义

module对象的directive方法的实质

首先我们得知道模块对象是什么?不清楚的可以看下这篇文章angular指令流程提到的setupModuleLoader函数。

其实模块对象的directive方法只是一个link,并没有真正的指令注册,这是什么意思呢?

先来看看它的定义:

 directive: invokeLater('$compilePRovider', 'directive')

再看看invokeLater方法的实现:

function invokeLater(provider, method, insertMethod, queue) {  if (!queue) queue = invokeQueue;  return function() {    queue[insertMethod || 'push']([provider, method, arguments]);    return moduleInstance;  };}

这样其实就很明了了,module对象的directive方法当调用时实质上只是做一个队列(invokeQueue)的存储,并没有正真意义上的指令注册。

就拿myDirective指令来说,存储的就是['$compileProvider', 'directive', ['myDirective', function() {...}]]这样一个数据元单位。

那么真正的指令注册是在什么时候呢,以及怎么注册的呢?在angular指令流程源码注释里也提到过:

当angular执行bootstrap()方法时,内部会调用loadModules()这个方法,来加载所有的module对象(这里就包括了myApp模块对象),模块加载的实质其中就包括遍历当前模块对象上的invokeQueue队列这一项,取出每一个数据元单位,然后执行对应服务的对应方法,所以这里的指令注册便会link到$compileProvider服务的directive()方法。

于是乎,便可以总结了:module.directive()方法只是指令的存储,指令的注册是在angular应用bootstrap()后,通过调用$compileProvider.directive()方法实现的。

其实module对象的其它方法也都是像这样延后执行(invokeLater)的(例如:module.config()module.controller()module.provider()等),这样的好处我认为是基于模块化的思想,即我们的指令(directive),控制器(controller),服务(service)等都是基于模块(module)的,angular程序的启动可以有选择性地加载想要的模块(即拥有了挂载在模块上的指令,控制器和服务等),这也是angular的第三方插件经常做的事情,声明一个自己的模块(如:ui.router),然后在(依赖)模块之上注册各种服务(如:$UrlRouterProvider)等供外界调用.

ok,当我们知道了指令注册的实质,我们便可以这样注册myDirective指令:

angular.module('myApp', [])  .config(['$compileProvider', function ($compileProvider) {    $compileProvider.directive('myDirective', function() {      return {        restrict: 'A',        replace: true,        scope: true,        template: '<span>hello world</span>',        compile: function (tElement) {          console.log('complile: ', tElement);          return function (scope, elem) {            console.log('link: ', elem);          }        }      }    });  }]);

不过,显然还是第一种方式来的更简便,也是推荐的使用方式。

$compileProvider服务directive方法的实现

上面我们知道了指令注册的实质是$compileProvider.directive(),那就赶紧来看看它源代码实现吧:

 // 指令存储容器,一个指令可以有多个指令工厂函数(即多个指令对象) // 格式如下: // { //     directive1: [fn1, fn2,..], //     directive2: [fn3, fn4,..] //     ... // } var hasDirectives = {};  this.directive = function registerDirective(name, directiveFactory) {  assertNotHasOwnProperty(name, 'directive');  // 单个指令注册  if (isString(name)) {    assertArg(directiveFactory, 'directiveFactory');    // 如果该指令还没有任何一个指令工厂    if (!hasDirectives.hasOwnProperty(name)) {      // 先初始化      hasDirectives[name] = [];      // 将该指令注册为服务,也就是说当我们通过$injector服务来获取该服务返回的指令对象集合(注意:是有缓存的单例哦)      $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',        function($injector, $exceptionHandler) {          // 指令对象集合          var directives = [];          // 循环遍历指令工厂集合,并收集每个工厂函数返回的指令对象          forEach(hasDirectives[name], function(directiveFactory, index) {            try {              // 调用工厂函数,注意这里用的是$injector,所以工厂函数也可以是一个拥有依赖注入的函数或数组              var directive = $injector.invoke(directiveFactory);              // 如果返回的directive是函数,那么被认为是指令对象的compile字段,              // 该函数返回的结果将作为指令对象的link字段              if (isFunction(directive)) {                directive = { compile: valueFn(directive) };             // 其他的则认为directive是指令对象             // 如果compile字段不存在,link字段存在的情况              } else if (!directive.compile && directive.link) {                directive.compile = valueFn(directive.link);              }              // 下面是指令对象的字段一些默认值设置,这些字段的含义之后再说              directive.priority = directive.priority || 0;              directive.index = index;              directive.name = directive.name || name;              directive.require = directive.require || (directive.controller && directive.name);              directive.restrict = directive.restrict || 'EA';              // 存储到指令对象集合中              directives.push(directive);            } catch (e) {              $exceptionHandler(e);            }          });          // 返回到指令对象集合          return directives;        }]);    }        // 存储当前指令工厂    hasDirectives[name].push(directiveFactory);  // 多个指令对象的形式注册,如:{'d1': function(){}, d2: ['$injector', function($injector) {}]}     } else {    forEach(name, reverseParams(registerDirective));  }    // 提供链式调用  return this;};

ok,结合上面的源代码加注释,其实可以知道一下几点:

  1. 注册指令时,注册的是工厂函数(支持依赖注入),它负责返回指令对象
  2. 一个指令可以注册多个工厂函数,就意味着将对应多个指令对象(即指令对象集合),其实多个指令对象之间是有一些冲突的,比如只能拥有有一个模板,拥有一个孤立作用域等
  3. 一个指令对应的指令对象集合是通过注册为服务的方式被外界获取的,比如我们可以这样获取上面例子中的myDirective指令集合(代码在这里):
angular.module('myApp').run(['$injector', function($injector) {  console.log($injector.get('myDirective' + 'Directive'));}]);

另外,工厂函数是支持依赖注入的,所以我们注册指令的形式就可以有那么几种:

// 普通的指令app.directive('myDirective', function () {  return function () {    console.log('postLink1');  }});// 隐式注入app.directive('myDirective', function ($injector) {  return function () {    console.log('postLink2: ', $injector);  }});// 显式注入app.directive('myDirective', ['$injector', function ($injector) {  return function () {    console.log('postLink3: ', $injector);  }}]);
指令对象的各个字段

作为使用者,在创建指令的时候,我们其实只要关注工厂函数返回的那个对象(我们称之为指令对象)就可以了,因为在指令编译的时候,它的每个字段将决定着你创建的指令会拥有哪些特性。

对于这些字段含义,打算在后面的编译指令时结合源码,举例讲解会比较清楚些。

指令详解

不知道大家是否还记得angular开始编译的入口,在scope原理的开头有提到过:

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',   function bootstrapApply(scope, element, compile, injector) {    scope.$apply(function() {      element.data('$injector', injector);      compile(element)(scope); // 这里这里开始编译    });
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表