AngularJS的启动引导过程
原文:http://www.angularjs.cn/A137?utm_source=ourjs.com
目录:
- 引导之前
- 自动引导启动框架
- 手工引导启动框架
- 引导第1步:创建注入器
- 引导第2步:创建根作用域
- 引导第3步:编译DOM子树
- 编译器/$compile
- 指令/directive
- 指令的规范化
1.引导之前:库阶段
在示例中,我们定义了一个指令ez-duang, 它应该会展开成一个动画 显示出来。 AngularJS代码:
angular.module("ezstuff",[]) .directive("ezDuang",function(){ return { restrict : "E", template : "<img src='http://ww4.sinaimg.cn/bmiddle/757eb2ffjw1eptcr4qobjg209205dthh.gif'>" }; });
html代码:
-
应该在下面看到一幅动画才对!
但是,看起来没有什么动画显示出来。AngularJS似乎没有工作,为什么?
有点像操作系统,AngularJs也有一个启动引导的概念。
当你在HTML文件中引入angular.min.js时,AngularJS只是建立了一个全局的 angular对象,这个对象有一些方法可供开发者调用,但应用的框架还没有建立。
在这个阶段,AngularJS还只是一个库,和jQuery类似,你可以使用angular.element() 操作DOM,也可以使用angular.injector()创建注入器... 但是,你定义的指令,你 创建的控制器,你封装的服务,你开发的模板...所有这些组件,还静静地躺在那里, 没有被整合在一起。
我们说,框架还没有运转起来,现在还是库阶段。
只有通过启动引导,AngularJS框架才开始将那些组件拼接在一起,应用才真正 开始运转。
像下面这样,试着给html元素增加一个ng-app指令,再重新运行!
<html ng-app="ezstuff"> .... </html>
2.自动引导启动框架
就像你看到的那样,如果HTML模板中有某个标签有ng-app属性,那么当DOM树建立成功后, AngularJS就会自动进入引导过程,启动整个框架:
试着把ng-app指令挪到body元素上,看看有什么不同?
3.手工引导启动框架
在大多数情况下,我们都使用ng-app指令来进行自动引导启动,但是如果一个HTML文件中 有多个ng-app,AngularJS只会自动引导启动它找到的第一个ng-app应用,这是需要手工引导 的一个应用场景。
我们可以利用 angular.bootstrap()
方法进行手动引导:
angular.bootstrap(element, [modules], [config]);
bootstrap方法有三个参数:
element : 一个DOM元素,以这个元素为Angular应用的根,等同自动引导时ng-app所在 的元素。这个参数是必须的。比如:document、document.body等。 modules : 引导时需要载入的模块数组。比如:[]、["ezstuff"]等。由于我们的HTML中引用 了ezstuff模块中定义的ez-duang指令,所以,我们需要指定载入ezstuff模块。 config :引导配置项,可选。我们先忽略。 最终,我们使用如下的形式进行手动引导:
angular.bootstrap(document,["ezstuff"]); 请点击【手动引导】按钮启动引导过程!
4.引导第1步:创建注入器
引导过程使AngularJS从库转变成了一个框架。
回忆我们之前提到,AngularJS深入骨髓地使用着依赖注入,那么,在引导过程 之初,首先需要创建一个注入器就毫不奇怪了。
注入器是通向AngularJS所有功能的入口,而AngularJS的功能实现,是通过模块的方式组织的。所以, 在创建注入器的时候,需要告诉AngularJS载入哪些模块(ng模块是内置载入的,不需要显式指定)。
在自动启动引导的场景下,可以给ng-app赋值以指定一个需要载入的模块,比如: ng-app = "ezstuff"
在手动启动引导的场景下,通过bootstrap方法的第二个参数指定需要载入的模块,比如: angular.bootstrap(document,["ezstuff"]);
INSIDE:无论自动启动还是手工启动,最终都是调用angular对象上的injector()方法创建了一个 注入器,然后把这个注入器存入了根对象的data里: var injector = angular.injector(["ng","ezstuff"]); angular.element(document).data("$injector",injector);
我们在开始模拟引导启动过程的第一步:创建注入器。
5.引导第2步:创建根作用域
scope对象是AngularJS实现数据绑定的重要服务,所以,在引导启动建立了注入器之后, AngularJS马上在应用的根节点上创建一个根作用域:$rootScope对象。
如果是自动引导启动,那么ng-app所在的DOM节点对应着根作用域。如果是手工引导启动, 那么在bootstrap方法中指定的第一个参数就对应着根作用域。
无论哪一种情况,一旦$rootScope对象创建成功,AngularJS就将这个对象存储到根节点 的data中,我们可以使用如下的方法查看这个对象: angular.element(approot).data("$rootScope");
你可以摆弄一下代码,看看$rootScope到底是什么东西。
6.引导第3步:编译DOM子树
引导过程的最后一步,是以ng-app所在DOM节点为根节点,对这棵DOM子树进行编译。
编译过程通常借助于指令,完成这几种操作:
- 对DOM对象进行变换。
- 在DOM对象上挂接事件监听。
- 在DOM对象对应的scope对象上挂接数据监听。 编译过程是AngularJS相当有特点的一个存在,我们将在下一节继续深入。
现在你应该看到结果了吧?
7.编译器/$compile
编译器$compile是一个AngularJS的内置服务,它负责遍历DOM树来查找匹配指令, 并调用指令的实现代码进行处理。
HTML编译包括3个步骤:
匹配指令 $compile遍历DOM树,如果发现有元素匹配了某个指令,那么这个指令将被加入 该DOM元素的指令列表中。一个DOM元素可能匹配多个指令。
执行指令的编译函数 当一个DOM元素的所有指令都找齐后,编译器根据指令的优先级/priority指令进行排序。 每个指令的compile函数被依次执行。每个compile执行的结果产生一个link函数,这些 link函数合并成一个复合link函数。
执行生成的链接函数 $compile通过执行指令的link函数,将模板和scope链接起来。结果就是一个DOM视图和scope对象模型 之间的动态数据绑定。
为何将编译和连接两个步骤分开?
简单说,当数据模型的变化会导致DOM结构变化时,指令就需要分别定义compile()函数和link函数。 例如,ng-repeat指令需要为数据集合中的每个成员复制DOM元素。将编译和链接过程分开可以有效 地提高性能,因为DOM的复制放在compile()里,仅需要执行一次,但链接则发生在每个生成的DOM元素 上,所以指令的link()函数会执行多次。
指令很少需要compile函数,因为大多数指令考虑的是作用于特定的DOM元素实例,而不是改变DOM 的结构。所以link函数更常用。
8.指令/directive
笼统地说,指令是DOM元素(例如属性、元素、CSS类等)上的标记符,用来告诉AngularJS的HTML编译器 ($compile服务)将特定的行为绑定到DOM元素,或者改变DOM元素。
指令可以放置在元素名、属性、CSS类名称及备注中。下面是一些等效的触发"ng-bind"指令的写法:
<span ng-bind="exp"></span>
<span class="ng-bind: exp;"></span>
<ng-bind></ng-bind>
<!-- directive: ng-bind exp -->
指令的实现本质上就是一个类工厂,它返回一个指令定义对象,编译器根据这个指令定义对象进行操作。
问题是,HTML中的ez-duang,怎么就匹配到了JavaScript中的ezDuang?
9.指令的规范化
AngularJS在进行匹配检测之前,首先对HTML元素的标签和属性名转化成规范的驼峰式字符串:
- 去除名称前缀的x-和data-
- 以: , - 或 _ 为分割符,将字符串切分成单词,除第一个单词外,其余单词首字母大写
-
重新拼接各单词 例如,下面的写法都等效地匹配ngBind指令:
<span ng-bind="name"></span> <br> <span ng:bind="name"></span> <br> <span ng_bind="name"></span> <br> <span data-ng-bind="name"></span> <br> <span x-ng-bind="name"></span> <br>
所以,在前面的课程中,我们在HTML中使用的ez-duang指令,将被规范为ezDuang, 编译器使用这个规范化的名称与注册的指令进行匹配。