Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 04. 指令

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

指令(directive)

指令(directive)是教会HTML新技能的方法。在DOM编译期间,与HTML相匹配的指令会执行。这样,我们就可以利用指令来注册一些动作,或者执行转换DOM的操作。

Angular内置了一些非常有用、并且可被扩展的指令,这样我们就能把HTML变身为DSL(可声明的领域专用语言)。

从HTML中调用指令

指令的命名方式采用驼峰式,如'ngBind'。但在调用时,可采用“蛇行式”,以及特定的分隔符::, -, _。甚至可以在它们前面加上x-data-以便通过HTML格式验证。

这里举几个有效的例子:

ng:bind, ng-bind, ng_bind, x-ng-bind, data-ng-bind

指令可被放置于以下位置:

  1. 元素名称2. 属性名称3. css类名称4. 注释

以下几个例子都是完全相同的调用指令myDir的方法。(不过大多数情况,我们仅会使用属性式指令)

<span my-dir="exp"></span>
<span class="my-dir: exp;"></span>
<my-dir></my-dir>
<!-- directive: my-dir exp -->

指令可被多种不同的方式调用,但在下面例子的测试代码中,它们是完全相同的。

Demo Source Code

index.html

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl1">
      Hello <input ng-model='name'> <hr/>
      <span ng:bind="name"> <span ng:bind="name"></span> <br/>
      <span ng_bind="name"> <span ng_bind="name"></span> <br/>
      <span ng-bind="name"> <span ng-bind="name"></span> <br/>
      <span data-ng-bind="name"> <span data-ng-bind="name"></span> <br/>
      <span x-ng-bind="name"> <span x-ng-bind="name"></span> <br/>
    </div>
  </body>
</html>

script.js

function Ctrl1($scope) {
  $scope.name = 'angular';
}

End to end test

it('should show off bindings', function() {
  expect(element('div[ng-controller="Ctrl1"] span[ng-bind]').text()).toBe('angular');
});

你可以在jsfiddle上查看并修改该示例

字符串插补(interpolation)

在编译阶段,编译器使用$interpolate服务来匹配文本和属性,检查它们是否嵌入了表达式。这些表达式注册为“观察者”,在digest周期中将被更新。

以下是一个interpolation的例子:

Hello {{username}}!

编译过程和指令(directive)匹配

HTML的编译过程,发生在三处:

  1. 首先HTML由标准浏览器api解析为DOM。认识到这一点很重要,因为模板代码必须为可解析的HTML。多数模板系统操作字符串,与它们不同的是,angularjs操作DOM元素。

  2. 使用$compile()方法对DOM进行编译。该方法遍历DOM和寻找可匹配的指令(directive),找到后则把它加到该DOM关联的指令列表中。一旦某DOM所有的指令都被找到,则按照指令中定义的优先级对它们进行排序,并调用它们的@compile()方法。该编译方法有机会修改DOM结构,并且有责任提供一个link()函数(将在后面解释)。$compile()方法返回一组连接(linking)方法,它们是一个集合,由该DOM关联的所有指令返回的link函数组成。

  3. 调用在上一步中返回的连接(link)函数,将模板与域对象(scope)连接起来。这将依次调用每一个指令(directvie)的连接函数,允许它们在元素上注册任意的监听器(listener)。结果将得到一个在域对象与DOM之间的实时绑定(live binding)。当域对象(scope)变化了,将立刻反映到DOM上。

    var $compile = …; // injected into your code
    var scope = …;

    var html = '

    ';

    // 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.
    linkFn(scope);

为什么要把编译(compile)与链接(link)分开

现在你也许会好奇,为什么我们要把编译过程分解为编译和连接两个步骤。为了理解这点,让我们看一下真实世界中的repeater的例子:

Hello {{user}}, you have these actions:
<ul>
  <li ng-repeat="action in user.actions">
    {{action.description}}
  </li>
</ul>

简单来说,只要当模型类改变时需要更新DOM结构(就像repeater),我们就需要将编译与连接分开。

上面的例子在编译时,编译器访问每一个节点来寻找指令(directive)。{{user}}是插补指令的一个例子,ngRepeat是另一种。

ngRepeat有一个困境:它需要为user.actions中的每一个action快速生成一个新的<li>,这表示它需要持有li元素的一个干净的copy用来复制,每当有新的actions插入时,li模板元素需要被快速复制被插入到ul中。

但是仅仅复制是不够的,它还需要编译li,所以它的指令(如{{action.descriptions}}也需要在正确的域(scope)中执行一遍。有一种很天真的处理方式是简单地插入一个li元素的copy并编译它。但是编译每一个li元素会很慢,因为编译过程需要遍历DOM树并寻找指令还要运行它们。如果我们把编译过程放在一个需要展示100个元素的repeater里,我们很快就会遇到性能问题。

解决方案是把编译过程分解为两步:第一步编译,寻找所有的指令并给它们按优先级排序;第二步连接,将把li的某实例与scope的某实例连接起来。

ngRepeat的运行方式不是把编译过程下放到li元素中,而是它们分开编译。结果是得到一个连接函数,它包括了li元素中所有将被附加到某一个指定的li的复制体上的指令。在运行期,ngRepeat会监视表达式,每当有新的元素添加到数组中时,它将复制li元素,为它创建一个新的域对象,并调用li元素上的连接函数。

总结:

编写指令(精简版)

在本例中,我们将创建一个显示当前时间的指令。

源代码

index.html

<!doctype html>
<html ng-app="time">
  <head>
    <script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl2">
      Date format: <input ng-model='format'> <hr/>
      Current time is: <span my-current-time="format"></span
    </div>
  </body>
</html>

script.js

function Ctrl2($scope) {
  $scope.format = 'M/d/yy h:mm:ss a';
}

angular.module('time', [])
  // Register the 'myCurrentTime' directive factory method.
  // We inject $timeout and dateFilter service since the factory method is DI.
  .directive('myCurrentTime', function($timeout, dateFilter) {
    // return the directive link function. (compile function not needed)
    return function(scope, element, attrs) {
      var format,  // date format
          deferId; // deferId, so that we can cancel the time updates

      // used to update the UI
      function updateTime() {
        element.text(dateFilter(new Date(), format));
      }

      // watch the expression, and update the UI on change.
      scope.$watch(attrs.myCurrentTime, function(value) {
        format = value;
        updateTime();
      });

      // schedule update in one second
      function updateLater() {
        // save the deferId for canceling
        deferId = $timeout(function() {
          updateTime(); // update DOM
          updateLater(); // schedule another update
        }, 1000);
      }

      // listen on DOM destroy (removal) event, and cancel the next UI update
      // to prevent updating time ofter the DOM element was removed.
      element.bind('$destroy', function() {
        $timeout.cancel(deferId);
      });

      updateLater(); // kick of the UI update process.
    }
  });

上例可在jsfiddle上查看效果及修改

编写指令(完整版)

这里是一个指令的结构骨架,完整的解释在后面。

var myModule = angular.module(…);

myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {

priority: 0,
template: '<div></div>',
templateUrl: 'directive.html',
replace: false,
transclude: false,
restrict: 'A',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
  return {
    pre: function preLink(scope, iElement, iAttrs, controller) { ... },
    post: function postLink(scope, iElement, iAttrs, controller) { ... }
  }
},
link: function postLink(scope, iElement, iAttrs) { ... }

};
return directiveDefinitionObject;
});

在大多数情况下,你不需要如此彻底的控制,所以上面的例子可以简化。所以与本示例不同的地方,将在下面几节中讲解。在本节我们仅仅对该示例感兴趣。

第一步是简化代码,尽管使用默认值。所以上面的代码可以简化为:

var myModule = angular.module(…);

myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {

compile: function compile(tElement, tAttrs) {
  return function postLink(scope, iElement, iAttrs) { ... }
}

};
return directiveDefinitionObject;
});

大多数指令只关系它们自己的实例,而不关心模板的转换,所以可以进一步简化为:

var myModule = angular.module(…);

myModule.directive('directiveName', function factory(injectables) {
return function postLink(scope, iElement, iAttrs) { … }
});

工厂方法

工厂方法的责任是创建指令。它只会在编译器第一次匹配到某指令时,被调用一次。你可以在这里执行任意的初始化操作。该方法被$injector.invoke调用,所以可被注入(injectable),并遵守injection注解的所有规则。

指令定义对象

指令定义对象为编译器提供了一些必要信息。有以下属性:

http://jsfiddle.net/api/post/library/pure/

comments powered by Disqus