首页 > 代码库 > AngularJS -- HTML 编译器
AngularJS -- HTML 编译器
HTML Compiler Overview(HTML 编译器 概要)
AngularJS 的HTML编译器允许开发人员教浏览器一些新的HTML语法。编译器还允许你去附加行为(包括你自定义的)到任何的HTML元素或者属性,甚至是你自己创建的新的HTML元素或者属性上去。是不是很强大,是不是很鸡冻啊? *^_^* AngularJS会在扩展的指令中,去调用这些行为。
有很多时尚的声明式的方式去格式化HTML静态文档。比如,有个对象需要居中显示,我们不需要提供一个方法,告诉浏览器,当窗口变化的时候,我们怎么去划分窗口找到窗口的中心,怎么才能找到那个对象的中心,然后让两个中心对齐。 而是使用简单的一个 align=”center” 的属性到任何你需要居中的对象上去,来实现居中的行为。 这就是声明式语言的强大之处。。。
当然,不能只看它的好,声明式的语言也是有限的,有缺点的。他不允许你去教浏览器新的语法。 比如,没有比较简单的方法来让文本对齐浏览器的1/3处,而不是之前的居中对齐到1/2 处。 这时,我们就需要教浏览器一个这样的新语法。
AngularJS有一些基础的,公共的指令,你可以用到任何AngularJS项目中去,我们也非常期望你去创建属于你特定程序的指令(自定义指令) 。 这对于你的应用程序依旧是很重要的。
所有的编译过程都是在Web浏览器端完成的。给服务端减小了很多的压力哦!
编译器
编译器是一个AngularJS的服务,它贯穿了整个查找DOM树,以及属性的过程。编译过程一共有两个阶段:
1. 编译,遍历整个DOM树,然后收集所有的指令。然后返回一个链式函数。
2. 链接,从上面查找到的所有在这个应用程序中的指令(在scope范围内的 directive)和 视图来看,所有在scope 范围内的模型(model) 都会被反映到视图上去(监视变化,进行实时更新,其实它就是在里面注册了事件,观察变化)。 并且,所有的用户操作(修改,增加,删除),也都会被反映到scope的model上去。 这使得scope 的模型(model) 必须是唯一的,单例的。
有一些指令,比如:ng-repeat 为它循环的那个集合的所有对象克隆一次自己的DOM元素,这样在编译和链接阶段可以提高性能。因为克隆的模板只需要编译一次,然后为每一个元素克隆一个编译后的模板,然后为每个对象链接一次,就可以减少了很多次的编译,使得应用程序更加的高效了。
指令(directive)
指令是一个行为,在编译过程当中,当遇到这个指令的时候,就会去触发它一次。 指令可以使元素名,也可以是属性,class 样式类名,甚至可以使组件。 这里有个相当于调用了 ng-bind 指令的例子:
<span ng-bind="exp"></span><span class="ng-bind: exp;"></span><ng-bind></ng-bind><!-- directive: ng-bind exp -->
指令只是一个函数,当在编译阶段在DOM树中被查找到后,就会被执行一次。 如果你没有看过指令那章,你必须去阅读一下关于指令的API,包括了如果自定义指令,点击这里阅读指令。
我们来看看下面的这个可拖拽的指令,请注意<span>元素的draggable
属性:
<div ng-app="drag"> <span draggable>Drag ME</span></div><script src="https://code.angularjs.org/1.3.0/angular.min.js"></script><script type="text/javascript"> (function(){ angular.module(‘drag‘, []). directive(‘draggable‘, function($document) { return function(scope, element, attr) { var startX = 0, startY = 0, x = 0, y = 0; element.css({ position: ‘relative‘, border: ‘1px solid red‘, backgroundColor: ‘lightgrey‘, cursor: ‘pointer‘, display: ‘block‘, width: ‘65px‘ }); element.on(‘mousedown‘, function(event) { // Prevent default dragging of selected content event.preventDefault(); startX = event.screenX - x; startY = event.screenY - y; $document.on(‘mousemove‘, mousemove); $document.on(‘mouseup‘, mouseup); }); function mousemove(event) { y = event.screenY - startY; x = event.screenX - startX; element.css({ top: y + ‘px‘, left: x + ‘px‘ }); } function mouseup() { $document.off(‘mousemove‘, mousemove); $document.off(‘mouseup‘, mouseup); } }; });})();</script>
这个拖拽的属性可以用于任何的元素,在这个元素上充当一个行为配置。 在某种程度上来说,我们相当于给浏览器扩展了新的语法。
理解视图(重要*****)
大多数的其它模板系统都是使用一个静态的字符串来充当模板,并且将数据和模板进行组合,生成一个新的字符串,然后被放到某一个元素的innerHTML中去的原理来实现的。如图:
这样的模板系统意味着当有任何更改需要反映到界面中的话,就要让模板和数据再次组合,生成新的静态字符串,然后再放到DOM元素的innerHTML属性中去,进行更新界面。这样的方法有一些问题:
1. 读取用户的输入,还有合并数据。
2.是通过重写的方式,去响应用户的输入的。
3.要管理整个更新过程。
4.没有行为控制能力。
AngularJS则不同于上面。AngularJS是去编译DOM树,而不是使用字符串模板。结果生成的是一个链式函数。当将scope中的Model和视图进行链接完毕了以后,开发人员则不再需要进行任何特殊的调用去更新视图了。由于没有使用innerHTML属性,你不会出现上面的模板系统会出现的突然删掉了用户输入的数据的问题。此外,AngularJS不仅包含了文本的绑定,也进行了行为的配置。
AngularJS的方法会产生一个稳定的DOM。DOM元素实例会在scope的生命周期内与scope的model(模型)实例进行绑定,这意味着我们可以使用代码区得到这个元素,也可以去给这个元素注册事件。也可以知道我们引用不会随着数据与模板的合并而被销毁掉了(上面的模板系统就存在这样的问题)。
指令是如何被编译的呢?
特别需要注意的是,AngularJS操作的是DOM节点,而不是string字符串哦! 一般来说,你很容易忘记这一点。因为当页面加载的时候,web浏览器会自动的去解析HTML为 DOM。
HTML的编译有三个阶段:
1. $compile编译贯穿了这个DOM树,并且匹配了所有的指令。 如果编译器发现了一个元素匹配到了一个指令,它就会把这个指令添加到一个记录某个DOM元素匹配到了哪些指令的集合中去。当然咯,一个元素是可以匹配到多个指令的。
2. 一旦所有的元素匹配完成指令了。编译器会根据指令的优先级(priority)进行分类。 每个指令的编译函数(compile
)会被调用执行。每个编译函数(compile
)都有机会去修改,操作DOM元素。每个编译函数(compile
)返回一个链接函数(link )。这些链接函数会被组合成一个函数链。然后这个函数链会去调用返回的所有的链接函数(link )。
3. $compile编译器会去组合通过上一步返回的函数链和scope范围内的模板。然后去调用自定义的指令的链接函数(link) ,然后给元素注册监听器,再为scope范围中的所有指令配置$watchs 。
将scope和DOM进行了绑定之后,返回的是一个可活动的结果;基于这一点,当编译完成后,scope中的model的任何改变都会被反映到DOM元素上去。
下面这个例子是一个使用了 $compile 服务的项目,这有助于你从AngularJS的观点去理解。
var $compile = ...; // injected into your codevar scope = ...;var parent = ...; // DOM element where the compiled template can be appendedvar html = ‘<div ng-bind="exp"></div>‘;// Step 1: parse HTML into DOM elementvar template = angular.element(html);// Step 2: compile the templatevar linkFn = $compile(template);// Step 3: link the compiled template with the scope.var element = linkFn(scope);// Step 4: Append to DOM (optional)parent.appendChild(element);
编译和链接的不同
此时,你肯定非常想知道一个编译过程为什么要分为两个部分,一个编译,一个链接阶段。 简单的回答就是: 为了在任何时候,一个模型的变化都会自动引起DOM结构的变化,我们就需要分为编译和链接两个阶段(好像等于没说)。
指令通常都会有一个链接功能(link function) 。 一个链接函数运行指令去注册监听指定的克隆的元素实例,不仅如此,而且还将scope的model也给复制了一份。
一个编译与链接的例子
为了方便理解,我们还是来直接看看代码吧:
Hello {{user.name}}, you have these actions:<ul> <li ng-repeat="action in user.actions"> {{action.description}} </li></ul>
当上面的例子在进行编译时,编译器会访问每一个节点,并且查询指令。 {{user.name}}会匹配到插入指令(也就是AngularJS自带的花括号绑定指令), ng-repeat 会匹配到 ngRepeat指令。
看看这句话:action
in user.actions
它会为每个元素克隆一个<li>元素。这看起来似乎是很简单的。但是如果你还要考虑到用户以后可能会增加或者删除元素的话,那就变得复杂了。也就是说,你必须要保存一份干净的备份,用于以后的克隆。。。
当一个新的 action
被插入进来了。那么就需要克隆一个<li>元素,然后插入到ul 中去。 但是你光是克隆 <li>元素可视不够的哦! 你同样的需要编译其中的指令,比如: {{action.description}},还要去计算一下它的scope范围。。。
有一个简单的方法去解决这个问题,那就是插入一个克隆的<li>元素,然后编译它。但是这种方法也有问题:我们编译每一个<li>元素的时候,我们需要做大量的克隆复制操作。说具体点,每次在进行遍历<li>进行克隆前,都要先去找到指令。这会导致编译过程变得缓慢。从而影响到了用户的再次插入行的节点。。
AngularJS的解决方案是将编译过程分为了两个阶段:
在编译阶段,所有的指令都被加上了标识,并且根据优先级(priority)进行排序,然后连接阶段,所有的在scope中的特定实例的 link 和 指定的<li>实例将会被执行。。
注意: 连接 (link) 意味着会将给DOM天剑监听事件,并且配置到了 $watch 列表中,以保证他们会进行异步同步。
相反, ngRepeat指令会先编译<li>。编译后返回一个包涵了所有指令的<li>元素的链接函数,然后再将它给复制到指定的克隆的元素上去(这个克隆会创建一个自己的scope!!)。。。
理解Scope是如何让指令在它指定的范围内工作的
一个最常见的关于指令进行重用的例子。
下面是一个伪代码,描述了一个简单的对话框组件可能是如何工作的。
<div><button ng-click="show=true">show</button><dialog title="Hello {{username}}." visible="show" on-cancel="show = false" on-ok="show = false; doSomething()"> Body goes here: {{username}} is {{title}}.</dialog></div>
当点击 show 按钮的时候,会打开一个对话框。对话框有标题(title) ,标题的数据绑定的是username属性, 还有一个消息内容区域,用来展示我们的消息。
关于这个对话框模板的定义,应该大概是这样的:
<div ng-show="visible"><h3>{{title}}</h3><div class="body" ng-transclude></div><div class="footer"> <button ng-click="onOk()">Save changes</button> <button ng-click="onCancel()">Close</button></div></div>
我们来解决第一个问题:对话框的标题。但是我们希望我们对话框的标题是通过属性的方式进行绑定进来的。或者使用双花括号{{}}进行绑定进来的。(eg: “Hello {{username}}” | title=”username”)。此外,我们还希望onOK 和 onCancel 按钮的函数式在我们的控件的scope中。 但是这就限制了这个控件的使用范围了。解决的方法是我们使用控件自己的scope去创建一个 全局的变量,代码大概如下:
scope: { title: ‘@‘, // the title uses the data-binding from the parent scope onOk: ‘&‘, // create a delegate onOk function onCancel: ‘&‘, // create a delegate onCancel function visible: ‘=‘ // set up visible to accept data-binding}
在控件自己的Scope中创建局部变量产生了两个问题:
1.分离------ 如果忘记了设置 title 属性,那么模板就会默认绑定到了父级的scope上去了。这是很不好的,是不可预测的,也是我们不想要的!
2. 嵌入问题 --- 嵌入的DOM可以看到局部变量,这就可能会重写我们进行了嵌入绑定的属性。在我们上面的例子中, title 属性就会 重写掉嵌入的title属性。
为了解决这个隔离问题,我们的指令 需要声明一个独立的scope 。在独立的scope中,没有派生自父级,所以我们就不用担心会有任何意外的重复的属性了,我的就是我的,不会用到父级的属性。
还有但是,我们使用这种方式又会产生另外一个问题:如果一个嵌入的DOM是一个独立scope的子节点,那么它将无法绑定到任何的东西。。 这就使得在独立scope中创建一个局部变量之前,嵌入的scope是原scope的子元素。 。(这句话感觉不会翻译,还是上原文,会的朋友,望指正。)
However isolated
scope creates a new problem: if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything. For this reason the transcluded scope is a child of the original scope, before the widget created an isolated scope for its local variables. This makes the transcluded and widget isolated scope siblings.
所以,最后指令的定义应该是这样的:
transclude: true,scope: { title: ‘@‘, // the title uses the data-binding from the parent scope onOk: ‘&‘, // create a delegate onOk function onCancel: ‘&‘, // create a delegate onCancel function visible: ‘=‘ // set up visible to accept data-binding},restrict: ‘E‘,replace: true
AngularJS -- HTML 编译器