zoukankan      html  css  js  c++  java
  • angular源码分析:$compile服务——指令的编写

    这一期中,我不会分析源码,只是翻译一下"https://docs.angularjs.org/api/ng/service/$compile",当然不是逐字逐句翻译,讲解指令应该如何编写,下一期再接着讲$compile的源码。我觉得,懂得如何使用angular可能对童鞋们更有用。
    先说点废话:上一期更新的时间是11月25日,一停就是相隔两周多了。1.是由于公司的网站上线(给公司打个广告(美好学院)[http://www.meihaoxueyuan.com]),2.是由于家里发生了一些事,3.是$compile做为angular的一个关键服务,tmd代码也太复杂了。

    $compile

    $compile的功能是,将一个html字符串或者一个DOM进行编译,最后返回一个链接函数,这个链接函数可以用于将作用域(Scope)和模版"链接"到一起.编译的过程,其实质是遍历DOM树,匹配和处理DOM上的各种指令的过程.
    注意:这个篇文档将深入介绍指令的各种选项,如果只是想通过例子来简单了解指令,可以参考(directive guide)[https://docs.angularjs.org/guide/directive]

    全面的指令接口(Comprehensive Directive API)

    对于指令(directive)有很多的不同选项.
    你可以通过directive定义的工厂函数返回一个"指令定义对象(Directive Definition Object)(用于定义指令的各种属性,如下)",或者仅仅返回一个postLink(所有其他属性将使用默认值);
    推荐使用Directive Definition Object形式
    这里有一个实例,用Directive Definition Object形式申明的指令

    var myModule = angular.module(...);
    
    myModule.directive('directiveName', function factory(injectables) {
      var directiveDefinitionObject = {
        priority: 0,
        template: '<div></div>', // or // function(tElement, tAttrs) { ... },
        // or
        // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
        transclude: false,
        restrict: 'A',
        templateNamespace: 'html',
        scope: false,
        controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
        controllerAs: 'stringIdentifier',
        bindToController: false,
        require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
        compile: function compile(tElement, tAttrs, transclude) {
          return {
            pre: function preLink(scope, iElement, iAttrs, controller) { ... },
            post: function postLink(scope, iElement, iAttrs, controller) { ... }
          }
          // or
          // return function postLink( ... ) { ... }
        },
        // or
        // link: {
        //  pre: function preLink(scope, iElement, iAttrs, controller) { ... },
        //  post: function postLink(scope, iElement, iAttrs, controller) { ... }
        // }
        // or
        // link: function postLink( ... ) { ... }
      };
      return directiveDefinitionObject;
    });
    

    注意:没有申明的属性选项将使用默认值,你可以在下面看到他们的默认值
    下面是一个可能简单一点的例子:

    var myModule = angular.module(...);
    
    myModule.directive('directiveName', function factory(injectables) {
      var directiveDefinitionObject = {
        link: function postLink(scope, iElement, iAttrs) { ... }
      };
      return directiveDefinitionObject;
      // or
      // return function postLink(scope, iElement, iAttrs) { ... }
    });
    

    指令定义对象(Directive Definition Object)

    指令定义对象将给编译器提供编译必要的说明。它包含这些属性:

    multiElement (多元素)

    当这个属性设置为true的时候,html编译器将会在含有directive-name-startdirective-name-end属性的Dom节点间收集Dom节点,被收集到的节点集合将作为一个整体被用作指令的元素(elements).推荐在没有严格行为(如ngClick)且不会操作或者替换子元素的的指令编写时,使用该属性.通过这个属性,我们可以给指令的操作元素制定一个范围,具体怎么用就看想象力了.

    priority (优先级)

    当大多数情况指令都被定义成只处理一个独立的DOM元素,有时候给指令排一个顺序是很有必要的.这个priority将用于编译函数执行前的指令排序.priority被定义为一个数值,这个数字越大,指令的优先级越高,就越是会被优先执行.Pre-link函数也会按照这个顺序来执行,但是post-link将会按照相反的顺序来执行.如果这个属性没有定义的话,默认值将是0.我们在查看angularjs的api文档中的指令时,可以注意一下每个指令的执行的优先级,比如ngIf的优先级是600,而ngShow的优先级是0,有的时候优先级会很重要.

    terminal (终结者,请允许我这么翻译它)

    如果这个属性设置为true,所有优先级小于该指令的指令都不会在执行了,注意和该指令优先级相同的元素依然会得到执行.

    scope (作用域)

    这个scope属性可以设置为true和对象或者falsy值(与false相等的值):

    • falsy:不会为指令建立新的作用域,指令将使用父作用域.
    • true :一个原型将继承父作用域的新的子作用域将会为这个指令建立.如果一个dom元素的多个指令都要求创建新的作用域,只有一个作用域会被创建.新建的作用规则将不再适用于模版的上层元素,因为模版总是新建一个作用域.
    • {...}(一个对象哈希):一个新的"孤立"作用域将会为指令创建."孤立"作用域不同与常规的作用,它不会在原型上继承父作用域.这对于创建一个可重用的组件很有帮助,因为它让指令不会意外的读到或修改到父作用域的数据.

    "孤立"作用域对象哈希定义了一个本地作用域的属性集合,这些属性能够从使用指令的元素的属性上派生出.这些本地的属性值,对于模版中的别名很有用.在对象哈希映射的键将会被定义为"孤立作用域"的的属性;映射的值将定义怎么和父作用域绑定到一起,通过匹配在使用的指令的的元素的属性.(为什么,翻译出来就这么绕呢,还是自己英语太搓了?)

    • @ 或者 @attr :绑定一个本地作用域到一个DOM的属性值.这回导致所有得到的值都是一个字符串.如果attr没有,绑定的属性将和本地属性同名.比如,有这样的一个DOM节点:<widget my-attr="hello {{name}}">;并且widget指令中有如此定义scope:{localName:'@myAttr'},那么widget的作用scope的属性localName将是hello {{name}}计算后的值.当name属性的值改变后,localName的值也会相应的改变.name则是从父作用域中获取的.
    • = 或者 =attr :设置双向绑定在作用域和父作用域间.如果attr没有定义,那么绑定的作用域名和使用指令的元素的属性名相同.当绑定指令的元素是<widget my-attr="parentModel">并且widget指令作用域定义为scope: { localModel:'=myAttr' },那么widget作用域属性localModel将映射为父作用域的parentModel,当改变parentModel时localModel也会相应改变,同样localModel改变时parentModel也会改变.如果父作用域中的属性不存在,angular会抛出一个NON_ASSIGNABLE_MODEL_EXPRESSION的异常.我们可以通过=?或者=?attr,来避免异常的抛出.如果你希望对属性使用"shallow watch",你可以使用=*或者=*attr(=*?或者=*?attr,如果属性是只是可选的).对于"shallow watch"在https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watchCollection有说明."shallow watch"将监视对象上的所有属性,任何属性变化都会导致回调.
    • & 或者 &attr :提供了一种执行父作用域下的表达式的方式.如果不指明attr,子作用域属性和dom元素的属性的名字将一致.当有使用的指令的dom元素<widget my-attr="count = count + value">和指令定义的作用域scope: { localFn:'&myAttr' },那么孤立作用域属性localFn将指向一个函数,这个函数包装了count = count + value表达式.这种方式比较适合子作用域通过表达式的方式从父作用域中获取值

    一般来说,可能多个指令同时作用于一个元素,但是在指令对作用域类型的要求上会存在一些限制.下面几点能有助于解释这些限制.为了简单起见,我们这里以两个指令为例,当然也适用于多个指令的情况:

    • no scope + no scope => 两个指令都没有要求自己的作用域,那么都会使用父作用域
    • child scope + no scope => 会共享使用独立的统一个子作用域
    • child scope + child scope => 会共享使用独立的统一个子作用域
    • isolated scope + no scope => 前者使用"孤立"作用域,后者使用父作用域
    • isolated scope + child scope =>不会正常工作
    • isolated scope + isolated scope => 不会正常工作

    bindToController

    当一个"孤立"作用域被用于一个组件,并且controllerAs也被使用了,bindToController: true将允许一个组件把他的属性绑定到控制器上,而不是scope上.当控制器被实例化,这些"孤立"作用域上的绑定的值就已经被初始化完成了.

    controller

    控制器构造函数.这个控制器将在pre-linking 阶段前被初始化,并且可以被其他的指令访问(参看require属性).这允许指令间的交互和协调相互的行为.控制器是可以使用下面本体变量进行"依赖注入的:

    • $scope 当前绑定到DOM元素的作用域
    • $element 当前的DOM元素
    • $attrs 当前的元素属性对象
    • $transclude 一个transclude链接函数预先绑定正确的transclusion 作用域:
      function([scope], cloneLinkingFn, futureParentElement, slotName):
      • scope: (可选)重载作用域
      • cloneLinkingFn:(可选)参数用于创建原始的transcluded 内容的克隆元素
      • futureParentElement (可选)
      • slotName:(可选)

    require

    要求另一个指令并且注入其控制器作为第4个参数传入链接函数.require这个属性要求传入的是一个指令名字的字符串或者字符串的数组.如果使用数组,注入的书序讲师数组中元素的顺序.如果相应的指令没有找到,指令没有控制器,一个错误将产生(除非链接函数没有定义,错误检测可以被忽略).指令名称可以使用的前缀:

    • 没有前缀 需要的指令在当前的DOM元素上.没有找到就抛出一个错误.
    • ? 尝试在当前的DOM元素上查找指令的controller,如果没有找到链接函数的第四个参数将被传入一个null
    • ^ 在当前的DOM元素和其父级元素上查找,如果没有找到报错
    • ^^ 只在父级元素上查找,没找到报错
    • ?^ 在当前DOM元素和父级元素上找,没有找到不报错,给link函数第四个参数传入一个null
    • ?^^ 在父级元素上找,没有找到不报错,给link函数第四个参数传入一个null

    controllerAs

    标识名用于引用指令作用域中的控制器.这允许控制器可以被在指令模版中被引用到.这尤其有用当指令作为一个组件来使用时,比如使用一个"孤立"作用域.这也是可能的,运用一个不使用"孤立"作用域的指令,但是你需要意识到,controllerAs引用可能重写父类的一些已经存在的属性.

    restrict

    要求一个字符串,这个字符串是 EACM的子集,这个用来限制指令使用样式.如果没有定义,默认是元素形式和元素属性形式

    • E Element name(default) :<my-directive></my-directive>
    • A Atrribute(default) :<div my-directive="exp"></div>
    • C Class:`
    • M Commment:<!-- directive:my-directive exp -->

    templateNamespace

    一个用于代表模版中标记语言使用的文档类型的字符.angular js 需要这些信息用于元素的创建和克隆的特殊处理,当他们被在外部定义为使用容器<svg><math>.

    • html 所有的根节点在模版中是html.根节点也可以是同样级别的元素比如<svg>或者<math>
    • svg 根节点在模版中是svg元素(不包括<math>)
    • math 根节点在模版中是MathML元素(不包括<svg>)

    如果templateNamespace 没有定义,默认是html.

    template

    HTML 标记,可以这样:

    • 替代指令元素的内容(默认)
    • 替代指令元素自己(如果replace是true ,但是这个属性可能在新版中会被删除)
    • 包裹指令元素的内容(如果transclude是true)
      其形式可以如下:
    • 一个字符串:比如<div red-on-hover>{{delete_str}}</div>
    • 一个包含两个参数(tElement,tAttrs)的函数,但是必须返回一个字符串

    templateUrl

    这个参数和template的功能类似,但是是通过一个明确的URL地址异步加载模版.
    因为模版加载是异步的,所以编译器将在模版被解析出来后才对元素上的执行进行编译.这意味着对于兄弟元素和父级元素的编译和链接想继续,就像这个元素上没有指令一样.

    编译器不支持整个编译过程等待模版的加载,是因为这将导致整个应用"假死"直到模版被异步加载成功,特别是当只有一个深度嵌套的指令拥有一个templateUrl属性的情况.

    模版的加载是异步的,即使模版被预先加载到了$templateCache中.

    你可以给templateUrl指定一个代表URL的字符串,也可以指定一个携带两个参数(tElment和tAttrs)(在后面的compile函数中将解释)并且返回一个url字符串的函数.在两种情况中,模版的Url都会通过$sce.getTrustedResourceUrl的处理.

    replace ([不赞成使用],将会在下一个主版本中移除,即v2.0)

    指出模版将替换什么内容,默认为false

    对于应用模块中,元素替换的需求只有很少的场景,主要的一中情况就是使用了svg上下文的可重用的定制组件(因为svg在DOM树中的定制元素是无法运行的).

    transclude

    提取元素内容在指令出现的地方,并且使之对指令有效.这里内容会被编译并且提供给指令作为transclusion函数,参看下面的Transclusion部分.

    compile

    function compile(tElement,tAttrs,transclude){ ... }
    

    编译函数用于对模版DOM的改造处理.然而,多数指令都没有对模版的改造处理,它不常被使用.这个编译函数接收的参数如下:

    • tElement 模版元素,就是指令声明所在的dom元素.仅对模版的改造处理是安全的在元素和子元素上.
    • tAttrs 模版属性,常规化后的属性列表在指令声明的元素上在指令所有指令的编译函数上共享
    • transclude [不推荐使用]一个transclude链接函数:function(scope,cloneLinkingFn)

    这个编译函数能够有一个返回值,可以是一个函数或者一个对象

    • 返回一个 (post-link)函数 这等同于注册一个链接函数通过link属性,当编译函数为空的时候.
    • 返回一个对象携带prepost属性注册的函数 允许你控制当链接函数被调用时在链接阶段.参看下面pre-linking 和post-linking函数的信息.

    这个属性只会在`compile函数没有定义的情况有效.

    function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
    

    链接函数负责注册DOM事件用于更新DOM.它会在模版被克隆后执行.这是大多数指令实现逻辑的地方.

    • scope 作用域. 作用域用于指令注册监听器(watches).
    • iElement 实例元素 使用指令的元素.处理元素的子节点只有在postLink函数中是安全的,因为子节点已经被克隆了.
    • iAttrs 实例属性 使用指令的元素的属性列表.
    • controller 指令所用要求的控制器的实例,这些实例没元素上的所有指令所共享,这允许指令将控制器作用一个通信频道使用.这个值的获取依赖于指令的require属性:
      • 没有控制器被要求:如果指令自己没有控制器,那么这个值讲师undefined.
      • string: 这个控制器的实例
      • array : 控制器的数组
        如果被要求的控制没有找到,并且它又是被定义了的,这个实例就是null,否则一个"Missing Required Controller`错误就会被抛出.
        注意你可以要指令自身的控制器,这个要求其他控制器是一样的
    • transcludeFn 一个transclude链接函数预绑定在正确的transclusion 作用域上。这个和指令控制器中$transclude是一样的,详情参考function([scope], cloneLinkingFn, futureParentElement)

    Pre-linking function

    在链接前执行。执行DOM的修改时不安全的,因为可能会导致编译链接函数在链接定位正确的元素失败.

    Post-linking function

    执行在子元素被链接后
    注意,子元素包含使用了templateUrl指令的情况将不会被编译和链接,直到等到他们的模版被异步加载成功后,而他们的编译和链接过程都很被挂起直到加载成功.
    在post-linking函数中处理DOM是安全的,不用等待异步模版被解析.

  • 相关阅读:
    为什么说 Java 程序员必须掌握 Spring Boot ?(转)
    Vert.x 之 HelloWorld
    Vert.x Web 文档手册
    Vert.x Core 文档手册
    Android数据存储五种方式总结
    设置简单的定时
    自定义dialog
    Android DrawerLayout 高仿QQ5.2双向侧滑菜单
    Fragments碎片
    理解Fragment生命周期
  • 原文地址:https://www.cnblogs.com/web2-developer/p/angular-14.html
Copyright © 2011-2022 走看看