angular
之所以使用起来很方便,是因为通常我们只需要在html里面引入一个或多个(自定义或内置的)指令就可以完成一个特定的功能(这也是angular推荐的方式),比如:一个简单的双向绑定
(用ng-model指令),或者模板循环渲染
(用ng-repeat指令),又或者是模板是否显示
(用ng-if指令),而对于这些指令的内部实现一般我们无需太多关心(除非你想深入了解),我们更乐意于把侧重点放在指令如何使用
(即API)上。
我们知道一个应用是由多个功能组成的,而在angular应用中,一个功能又是由一个或多个指令所完成,那么我们可以认为angular应用被很多指令所驱动着,当我们给angular应用添加了所有我们需要的指令后,angular内部则会负责帮我们编译和运行所有指令,从而完成特定功能的实现。
那么,对于指令,我们自然会有以下的疑问:
带着疑问,我们慢慢一步一步深入到源码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
作为元素属性
的方式调用这个指令(这里只是简单的模板替换和打印日志)。
这里其实我们关注的其实有两点:
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()
,那就赶紧来看看它源代码实现吧:
// 指令存储容器,一个指令可以有多个指令工厂函数(即多个指令对象) // 格式如下: // { // 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,结合上面的源代码加注释,其实可以知道一下几点:
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); // 这里这里开始编译 });
新闻热点
疑难解答