博弈AngularJS讲义(15) - HTML Compiler
Angular HTML编译器可以让开发者通过定制标签或者属性来拓展HTML语法。通过Angular HTML编译器,我们可以给任何HTML元素及其属性赋予额外的行为,丰富HTML的语义。Angular把这种行为扩展称作指令。
HTML使用了声明式的语法,例如我们只需在任何HTML元素上加上 align="center"属性即可让浏览器对元素进行居中对齐,而不用进行额外的编程来实现居中对齐。然而这种声明式的语言是有局限性的,HTML的语法集是有限的,浏览器引擎无法识别更多的语法。例如我们无法在HTML语法中声明让文本在离左侧1/3处对齐。
Angular框架提供了一些指令集丰富了HTML语义,方便我们构建应用。同时Angular还提供了一套机制让开发者自定义指令。在Angular应用启动阶段,HTML编译器会“编译”指令,这个过程发生在浏览器端,无需与服务端交互。
Angular编译器
编译器是Angular框架的一个服务组件,$compiler会遍历DOM树识别并处理指令,处理包含如下两个步骤:
1. 编译(compile): 遍历DOM树,识别收集指令,作为连接函数的输入。
2. 连接(linking):将指令绑定到作用域并生成动态的视图,即双向数据绑定。模型数据的变化会触发视图的更新,反之,用户与视图交互也会导致模型数据的更新。
一些指令会对集合类的模型做特殊的处理,例如ng-repeat会给集合中的每一项克隆同样的DOM元素,并创建子作用域。
指令
指令即AngularJS HTML编译过程中遇到特殊的HTML标记或者属性时触发的行为。指令可以在HTML标签,属性,CSS类名以及注释中。例如下面的ng-bind指令:
<span ng-bind="exp"></span> <span class="ng-bind: exp;"></span> <ng-bind></ng-bind> <!-- directive: ng-bind exp -->再看下面的例子,我们定义了一个draggable指令,赋予相应的元素可拖拽的行为。
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); } }; });这里我们扩展了span标签,通过draggable属性赋予其可拖拽的行为:
<span draggable>Drag ME</span>
运行结果:
理解视图
大多数模板系统都是将基于字符串的静态模板上的片段用模型数据替换。原理如下图:
这种单向数据绑定意味着每次模型数据更新后需要重新通过innerHTML的方式将数据与模板绑定。
Angular编译器处理基于DOM的模板,通过linking函数实现模板和模型的视图双向绑定。开发者无需添加任何代码手动更新视图。
指令的编译过程
Angular操作的是DOM节点,而不是静态的字符模板。通常, 我们都不会注意到这个约束,因为在页面加载是,浏览器会自动将HTML解析成DOM。
HTML编译可发生在如下三个阶段:
1. $compile遍历DOM匹配识别指令。
如过编译器发现HTML元素与指令匹配时,会将指令加入到与该DOM元素对应的指令集合里。也就是可以一个HTML元素可以有多个指令。
2. 当编译器识别出元素的所有指令后, 编译器会对匹配的指令按优先级排序,每个指令的compile方法会被相继执行,在执行过程中可以修改DOM。每个compile都会返回一个link方法,Angular会将返回的的link函数合并成一个大的link过程。
3. $compile服务组件会将模板与模型通过作用域连接起来, 通过调用指令的linking方法,在DOM元素上注册事件监听器并建立$watch相应的作用域。这样对作用域模型的更新将会即时更新到视图。下面关于$compile服务的代码将帮助我们更好的理解:
var $compile = ...; // injected into your code var scope = ...; var parent = ...; // DOM element where the compiled template can be appended var html = '<div ng-bind="exp"></div>'; // Step 1: parse HTML into DOM element var template = angular.element(html); // Step 2: compile the template var 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);编译与连接的区别
很少有指令有compile方法,因为大多数指令不会改变整个DOM元素的结构, 而是丰富元素的行为。指令通常都有link方法,来建立模型和作用域模型的双向同步机制。让我们看如下的例子:
Hello {{user.name}}, you have these actions: <ul> <li ng-repeat="action in user.actions"> {{action.description}} </li> </ul>Angular编译器将会识别出{{user.name}}和ng-repeat指令, ng-repeat需要迭代user.actions,给每个action克隆<li>元素, 当user.actions发生变化是情况将会变得更加复杂,为了提高克隆效率,需要将所有action共有的部分抽离出来,对于<li>需要一份干净的copy. 当新的action项添加进来时,会克隆一份<li>模板加入到<ul>中, 并为每个<li>创建独立的作用域。简单的做法是一次把<li>插入<ul>并进行编译,这样会导致很多重复的工作。 特别是每次都需要遍历li,频繁的操作会引发效率问题,使页面响应变慢。
为了提搞效率Angular将编译过程分成两个阶段:
- compile阶段识别出所有符合条件的指令,并按优先级排序。
- link阶段建立视图与模型的双向绑定。
理解作用域的工作原理
指令主要被用来构建可重用的组件。下面是一个简单的对话框的指令:
<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按钮打开对话框,对话框含有标题与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>
只有将对话框与作用域绑定才会正确工作, 该作用域中必须定义title属性以及onOk和onCancel, 而我们希望的是dialog有自己的作用域,减少对外部的依赖,达到完全的可重用。 为解决作用域映射的问题,我们使用scope创建局部属性:
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