zoukankan      html  css  js  c++  java
  • Angular进阶教程二

    6.2自定义指令详解

    angular的指令机制。angular通过指令的方式实现了HTML的扩展,增强后的HTML不仅长相焕然一新,同时也获得了很多强大的技能。更厉害的是,你还可以自定义指令,这就意味着HTML标签的范围可以扩展到无穷大。angular赋予了你造物主的能力。既然是作为angular的精华之一,相应的指令相关的知识也很多的。

    6.2.1指令的编译过程

      在开始自定义指令之前,我们有必要了解一下指令在框架中的执行流程:

    1.浏览器得到 HTML 字符串内容,解析得到 DOM 结构。

    2.ng 引入,把 DOM 结构扔给 $compile 函数处理:

    ① 找出 DOM 结构中有变量占位符;

    ② 匹配找出 DOM 中包含的所有指令引用;

    ③ 把指令关联到 DOM;

    ④ 关联到 DOM 的多个指令按权重排列;

    ⑤ 执行指令中的 compile 函数(改变 DOM 结构,返回 link 函数);

    ⑥ 得到的所有 link 函数组成一个列表作为 $compile 函数的返回。

    3. 执行 link 函数(连接模板的 scope)。

    这里注意区别一下$compile和compile,前者是ng内部的编译服务,后者是指令中的编译函数,两者发挥作用的范围不同。compile和link函数息息相关又有所区别,这个在后面会讲。了解执行流程对后面的理解会有帮助。

    在这里有些人可能会问,angular不就是一个js框架吗,怎么还能跟编译扯上呢,又不是像C++那样的高级语言。其实此编译非彼编译,ng编译的工作是解析指令、绑定监听器、替换模板中的变量等。因为工作方式很像高级语言编辑中的递归、堆栈过程,所以起名为编译,不要疑惑。

    6.2.2指令的使用方式及命名方法

      指令的几种使用方式如下:

    • 作为标签:<my-dir></my-dir>
    • 作为属性:<span my-dir="exp"></span>
    • 作为注释:<!-- directive: my-dir exp -->
    • 作为类名:<span class="my-dir: exp;"></span>

      其实常用的就是作为标签和属性,下面两种用法目前还没见过,感觉就是用来卖萌的,姑且留个印象。我们自定义的指令就是要支持这样的用法。

    关于自定义指令的命名,你可以随便怎么起名字都行,官方是推荐用[命名空间-指令名称]这样的方式,像ng-controller。不过你可千万不要用ng-前缀了,防止与系统自带的指令重名。另外一个需知道的地方,指令命名时用驼峰规则,使用时用-分割各单词。如:定义myDirective,使用时像这样:<my-directive>。

    6.2.3自定义指令的配置参数

    下面是定义一个标准指令的示例,可配置的参数包括以下部分:

    myModule.directive('namespaceDirectiveName', function factory(injectables) {

            var directiveDefinitionObject = {

                restrict: string,//指令的使用方式,包括标签,属性,类,注释

                priority: number,//指令执行的优先级

                template: string,//指令使用的模板,用HTML字符串的形式表示

                templateUrl: string,//从指定的url地址加载模板

                replace: bool,//是否用模板替换当前元素,若为false,则append在当前元素上

                transclude: bool,//是否将当前元素的内容转移到模板中

                scope: bool or object,//指定指令的作用域

            controller: function controllerConstructor($scope, $element, $attrs, $transclude){...},//定义与其他指令进行交互的接口函数

                require: string,//指定需要依赖的其他指令

    link: function postLink(scope, iElement, iAttrs) {...},//以编程的方式操作DOM,包

    括添加监听器等

                compile: function compile(tElement, tAttrs, transclude){

                    return: {

                        pre: function preLink(scope, iElement, iAttrs, controller){...},

                        post: function postLink(scope, iElement, iAttrs, controller){...}

                    }

                }//编程的方式修改DOM模板的副本,可以返回链接函数

            };

            return directiveDefinitionObject;

    });         

    看上去好复杂的样子,定义一个指令需要这么多步骤嘛?当然不是,你可以根据自己的需要来选择使用哪些参数。事实上priority和compile用的比较少,template和templateUrl又是互斥的,两者选其一即可。所以不必紧张,接下来分别学习一下这些参数:

    l 指令的表现配置参数:restrict、template、templateUrl、replace、transclude;

    l 指令的行为配置参数:compile和link;

    l 指令划分作用域配置参数:scope;

    l 指令间通信配置参数:controller和require。

    6.2.3指令的表现参数restrict

    指令的表现配置参数:restrict、template、templateUrl、replace、transclude。

    我将先从一个简单的例子开始。

        例子的代码如下:

    var app = angular.module('MyApp', [], function(){console.log('here')});

    app.directive('sayHello',function(){

    return {

         restrict : 'E',

    template : '<div>hello</div>'

    };

    })         

    然后在页面中,我们就可以使用这个名为sayHello的指令了,它的作用就是输出一个hello单词。像这样使用:

    <say-hello></say-hello>         

    这样页面就会显示出hello了,看一下生成的代码:

    <say-hello>

    <div>hello</div>

    </say-hello> 

       稍稍解释一下我们用到的两个参数,restirct用来指定指令的使用类型,其取值及含义如下:

    取值

    含义

    使用示例

    E

    标签

    <my-menu title=Products></my-menu>

    A

    属性

    <div my-menu=Products></div>

    C

    <div class="my-menu":Products></div>

    M

    注释

    <!--directive:my-menu Products-->

    默认值是A。也可以使用这些值的组合,如EA,EC等等。我们这里指定为E,那么它就可以像标签一样使用了。如果指定为A,我们使用起来应该像这样:

    <div say-hello></div>

    从生成的代码中,你也看到了template的作用,它就是描述你的指令长什么样子,这部分内容将出现在页面中,即该指令所在的模板中,既然是模板中,template的内容中也可以使用ng-modle等其他指令,就像在模板中使用一样。

    在上面生成的代码中,我们看到了<div>hello</div>外面还包着一层<say-hello>标签,如果我们不想要这一层多余的东西了,replace就派上用场了,在配置中将replace赋值为true,将得到如下结构:

    <div>hello</div>

       replace的作用正如其名,将指令标签替换为了temple中定义的内容。不写的话默认为false。

    上面的template未免也太简单了,如果你的模板HTML较复杂,如自定义一个ui组件指令,难道要拼接老长的字符串?当然不需要,此时只需用templateUrl便可解决问题。你可以将指令的模板单独命名为一个html文件,然后在指令定义中使用templateUrl指定好文件的路径即可,如:

    templateUrl : ‘helloTemplate.html’         

    系统会自动发一个http请求来获取到对应的模板内容。是不是很方便呢,你不用纠结于拼接字符串的烦恼了。如果你是一个追求完美的有考虑性能的工程师,可能会发问:那这样的话岂不是要牺牲一个http请求?这也不用担心,因为ng的模板还可以用另外一种方式定义,那就是使用<script>标签。使用起来如下:

    <script type="text/ng-template" id="helloTemplate.html">

         <div>hello</div>

    </script>        

     你可以把这段代码写在页面头部,这样就不必去请求它了。在实际项目中,你也可以将所有的模板内容集中在一个文件中,只加载一次,然后根据id来取用。

    接下来我们来看另一个比较有用的配置:transclude,定义是否将当前元素的内容转移到模板中。看解释有点抽象,不过亲手试试就很清楚了,看下面的代码(例06):

    app.directive('sayHello',function(){

    return {

         restrict : 'E',

    template : '<div>hello,<b ng-transclude></b>!</div>',

         replace : true,

          transclude : true

    };

    })         

    指定了transclude为true,并且template修改了一下,加了一个<b>标签,并在上面使用了ng-transclude指令,用来告诉指令把内容转移到的位置。那我们要转移的内容是什么呢?请看使用指令时的变化:

    <say-hello>美女</say-hello>

    内容是什么你也看到了哈~在运行的时候,美女将会被转移到<b>标签中,原来此配置的作用就是——乾坤大挪移!看效果:

    hello, 美女!

    这个还是很有用的,因为你定义的指令不可能老是那么简单,只有一个空标签。当你需要对指令中的内容进行处理时,此参数便大有可用。

    6.2.4指令的行为参数:compilelink

    6.2.3中简单介绍了自定义一个指令的几个简单参数,restrict、template、templateUrl、replace、transclude,这几个理解起来相对容易很多,因为它们只涉及到了表现,而没有涉及行为。我们继续学习ng自定义指令的几个重量级参数:compile和link

    l 理解compile和link

      不知大家有没有这样的感觉,自己定义指令的时候跟写jQuery插件有几分相似之处,都是先预先定义好页面结构及监听函数,然后在某个元素上调用一下,该元素便拥有了特殊的功能。区别在于,jQuery的侧重点是DOM操作,而ng的指令中除了可以进行DOM操作外,更注重的是数据和模板的绑定。jQuery插件在调用的时候才开始初始化,而ng指令在页面加载进来的时候就被编译服务($compile)初始化好了。

    在指令定义对象中,有compile和link两个参数,它们是做什么的呢?从字面意义上看,编译、链接,貌似太抽象了点。其实可大有内涵,为了在自定义指令的时候能正确使用它们,现在有必要了解一下ng是如何编译指令的。

    l 指令的解析流程详解

      我们知道ng框架会在页面载入完毕的时候,根据ng-app划定的作用域来调用$compile服务进行编译,这个$compile就像一个大总管一样,清点作用域内的DOM元素,看看哪些元素上使用了指令(如<div ng-modle=”m”></div>),或者哪些元素本身就是个指令(如<mydierc></mydirec>),或者使用了插值指令( {{}}也是一种指令,叫interpolation directive),$compile大总管会把清点好的财产做一个清单,然后根据这些指令的优先级(priority)排列一下,真是个细心的大总管哈~大总管还会根据指令中的配置参数(template,place,transclude等)转换DOM,让指令“初具人形”。

    然后就开始按顺序执行各指令的compile函数,注意此处的compile可不是大总管$compile,人家带着$是土豪,此处执行的compile函数是我们指令中配置的,compile函数中可以访问到DOM节点并进行操作,其主要职责就是进行DOM转换,每个compile函数执行完后都会返回一个link函数,这些link函数会被大总管汇合一下组合成一个合体后的link函数,为了好理解,我们可以把它想象成葫芦小金刚,就像是进行了这样的处理。

    //合体后的link函数

    function AB(){

      A(); //子link函数

      B(); //子link函数

    }  

    接下来进入link阶段,合体后的link函数被执行。所谓的链接,就是把view和scope链接起来。链接成啥样呢?就是我们熟悉的数据绑定,通过在DOM上注册监听器来动态修改scope中的数据,或者是使用$watchs监听 scope中的变量来修改DOM,从而建立双向绑定。由此也可以断定,葫芦小金刚可以访问到scope和DOM节点。

    不要忘了我们在定义指令中还配置着一个link参数呢,这么多link千万别搞混了。那这

    个link函数是干嘛的呢,我们不是有葫芦小金刚了嘛?那我告诉你,其实它是一个小三。此话怎讲?compile函数执行后返回link函数,但若没有配置compile函数呢?葫芦小金刚自然就不存在了。 

    正房不在了,当然就轮到小三出马了,大总管$compile就把这里的link函数拿来执行。这就意味着,配置的link函数也可以访问到scope以及DOM节点。值得注意的是,compile函数通常是不会被配置的,因为我们定义一个指令的时候,大部分情况不会通过编程的方式进行DOM操作,而更多的是进行监听器的注册、数据的绑定。所以,小三名正言顺的被大总管宠爱。

    听完了大总管、葫芦小金刚和小三的故事,你是不是对指令的解析过程比较清晰了呢?不过细细推敲,你可能还是会觉得情节生硬,有些细节似乎还是没有透彻的明白,所以还需要再理解下面的知识点:

    l compile和link的区别

      其实在我看完官方文档后就一直有疑问,为什么监听器、数据绑定不能放在compile函数中,而偏偏要放在link函数中?为什么有了compile还需要link?就跟你质疑我编的故事一样,为什么最后小三被宠爱了?所以我们有必要探究一下,compile和link之间到底有什么区别。好,正房与小三的PK现在开始。

    首先是性能。举个例子:

    <ul>

      <li ng-repeat="a in array">

        <input ng-modle=”a.m” />

      </li>

    </ul>         

    我们的观察目标是ng-repeat指令。假设一个前提是不存在link。大总管$compile在编译这段代码时,会查找到ng-repeat,然后执行它的compile函数,compile函数根据array的长度复制出n个<li>标签。而复制出的<li>节点中还有<input>节点并且使用了ng-modle指令,所以compile还要扫描它并匹配指令,然后绑定监听器。每次循环都做如此多的工作。而更加糟糕的一点是,我们会在程序中向array中添加元素,此时页面上会实时更新DOM,每次有新元素进来,compile函数都把上面的步骤再走一遍,岂不是要累死了,这样性能必然不行。

    现在扔掉那个假设,在编译的时候compile就只管生成DOM的事,碰到需要绑定监听器的地方先存着,有几个存几个,最后把它们汇总成一个link函数,然后一并执行。这样就轻松多了,compile只需要执行一次,性能自然提升。

    另外一个区别是能力。

    尽管compile和link所做的事情差不多,但它们的能力范围还是不一样的。比如正房能管你的存款,小三就不能。小三能给你初恋的感觉,正房却不能。

    我们需要看一下compile函数和link函数的定义:

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

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

    这些参数都是通过依赖注入而得到的,可以按需声明使用。从名字也容易看出,两个函数各自的职责是什么,compile可以拿到transclude,允许你自己编程管理乾坤大挪移的行为。而link中可以拿到scope和controller,可以与scope进行数据绑定,与其他指令进行通信。两者虽然都可以拿到element,但是还是有区别的,看到各自的前缀了吧?compile拿到的是编译前的,是从template里拿过来的,而link拿到的是编译后的,已经与作用域建立了

    关联,这也正是link中可以进行数据绑定的原因。

      我暂时只能理解到这个程度了。实在不想理解这些知识的话,只要简单记住一个原则就行了:如果指令只进行DOM的修改,不进行数据绑定,那么配置在compile函数中,如果指令要进行数据绑定,那么配置在link函数中。

    6.2.5指令的划分作用域参数:scope

    我们在上面写了一个简单的<say-hello></say-hello>,能够跟美女打招呼。但是看看人家ng内置的指令,都是这么用的:ng-model=”m”,ng-repeat=”a in array”,不单单是作为属性,还可以赋值给它,与作用域中的一个变量绑定好,内容就可以动态变化了。假如我们的sayHello可以这样用:<say-hello speak=”content”>美女</say-hello>,把要对美女说的话写在一个变量content中,然后只要在controller中修改content的值,页面就可以显示对美女说的不同的话。这样就灵活多了,不至于见了美女只会说一句hello,然后就没有然后。

    为了实现这样的功能,我们需要使用scope参数,下面来介绍一下。

    使用scope为指令划分作用域

      顾名思义,scope肯定是跟作用域有关的一个参数,它的作用是描述指令与父作用域的关系,这个父作用域是指什么呢?想象一下我们使用指令的场景,页面结构应该是这个样子:

    <div ng-controller="testC">

        <say-hello speak="content">美女</say-hello>

    </div>  

    外层肯定会有一个controller,而在controller的定义中大体是这个样子:

    var app = angular.module('MyApp', [], function(){console.log('here')});

    app.controller('testC',function($scope){

    $scope.content = '今天天气真好!';

    }); 

    所谓sayHello的父作用域就是这个名叫testC的控制器所管辖的范围,指令与父作用域的关系可以有如下取值:

    取值

    说明

    false

    默认值。使用父作用域作为自己的作用域

    true

    新建一个作用域,该作用域继承父作用域

    javascript对象

    与父作用域隔离,并指定可以从父作用域访问的变量

    乍一看取值为false和true好像没什么区别,因为取值为true时会继承父作用域,即父作用域中的任何变量都可以访问到,效果跟直接使用父作用域差不多。但细细一想还是有区别的,有了自己的作用域后就可以在里面定义自己的东西,与跟父作用域混在一起是有本质上的区别。好比是父亲的钱你想花多少花多少,可你自己挣的钱父亲能花多少就不好说了。你若想看这两个作用域的区别,可以在link函数中打印出来看看,还记得link函数中可以访问到scope吧。

    最有用的还是取值为第三种,一个对象,可以用键值来显式的指明要从父作用域中使用属性的方式。当scope值为一个对象时,我们便建立了一个与父层隔离的作用域,不过也不是完全隔离,我们可以手工搭一座桥梁,并放行某些参数。我们要实现对美女说各种话就得靠这个。使用起来像这样:

    scope: {

            attributeName1: 'BINDING_STRATEGY',

            attributeName2: 'BINDING_STRATEGY',...

    }  

    键为属性名称,值为绑定策略。等等!啥叫绑定策略?最讨厌冒新名词却不解释的行为!别急,听我慢慢道来。

     

      先说属性名称吧,你是不是认为这个attributeName1就是父作用域中的某个变量名称?错!其实这个属性名称是指令自己的模板中要使用的一个名称,并不对应父作用域中的变量,稍后的例子中我们来说明。再来看绑定策略,它的取值按照如下的规则:

    符号

    说明

    举例

    @

    传递一个字符串作为属性的值

    str : ‘@string’

    =

    使用父作用域中的一个属性,绑定数据到指令的属性中

    name : ‘=username’

    &

    使用父作用域中的一个函数,可以在指令中调用

    getName : ‘&getUserName’

      总之就是用符号前缀来说明如何为指令传值。你肯定迫不及待要看例子了,我们结合例子看一下,小二,上栗子~

    举例说明

    我想要实现上面想像的跟美女多说点话的功能,即我们给sayHello指令加一个属性,通过给属性赋值来动态改变说话的内容 主要代码如下:

    app.controller('testC',function($scope){

       $scope.content = '今天天气真好!';

    });

    app.directive('sayHello',function(){

        return {

            restrict : 'E',

    template: '<div>hello,<b ng-transclude></b>,{{ cont }}</div>',

            replace : true,

            transclude : true,

            scope : {

     

                 cont : '=speak'

             }

        };

    });

    然后在模板中,我们如下使用指令:

    <div ng-controller="testC">

        <say-hello speak=" content ">美女</say-hello>

    </div>

    看看运行效果:

    美女今天天气真好!

      执行的流程是这样的:

      ① 指令被编译的时候会扫描到template中的{ {cont} },发现是一个表达式;

      ② 查找scope中的规则:通过speak与父作用域绑定,方式是传递父作用域中的属性;

      ③ speak与父作用域中的content属性绑定,找到它的值“今天天气真好!”;

      ④ 将content的值显示在模板中。

    这样我们说话的内容content就跟父作用域绑定到了一其,如果动态修改父作用域的content的值,页面上的内容就会跟着改变,正如你点击“换句话”所看到的一样。

      这个例子也太小儿科了吧!简单虽简单,但可以让我们理解清楚,为了检验你是不是真的明白了,可以思考一下如何修改指令定义,能让sayHello以如下两种方式使用:

    <span say-hello speak="content">美女</span>

    <span say-hello="content" >美女</span>

      答案我就不说了,简单的很。下面有更重要的事情要做,我们说好了要写一个真正能用的东西来着。接下来就结合所学到的东西来写一个折叠菜单,即点击可展开,再点击一次就收缩回去的菜单。

    控制器及指令的代码如下(例07):

    app.controller('testC',function($scope){

            $scope.title = '个人简介';

        $scope.text = '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流';

    });

        app.directive('expander',function(){

            return {

                restrict : 'E',

                templateUrl : 'expanderTemp.html',

                replace : true,

                transclude : true,

                scope : {

                    mytitle : '=etitle'

                },

                link : function(scope,element,attris){

                    scope.showText = false;

                    scope.toggleText = function(){

                        scope.showText = ! scope.showText;

                    }

                }

            };

        });

    HTML中的代码如下:

     

    <script type="text/ng-template" id="expanderTemp.html">

        <div  class="mybox">

    <div class="mytitle" ng-click="toggleText()">

    {{mytitle}}

    </div>

    <div ng-transclude ng-show="showText">

    </div>

    </div>

    </script>

    <div ng-controller="testC">

        <expander etitle="title">{{text}}</expander>

    </div>

      还是比较容易看懂的,我只做一点必要的解释。首先我们定义模板的时候使用了ng的一种定义方式<script type=”text/ng-template”id="expanderTemp.html">,在指令中就可以用templateUrl根据这个id来找到模板。指令中的{{mytitle}}表达式由scope参数指定从etitle传递,etitle指向了父作用域中的title。为了实现点击标题能够展开收缩内容,我们把这部分逻辑放在了link函数中,link函数可以访问到指令的作用域,我们定义showText属性来表示内容部分的显隐,定义toggleText函数来进行控制,然后在模板中绑定好。 如果把showText和toggleText定义在controller中,作为$scope的属性呢?显然是不行的,这就是隔离作用域的意义所在,父作用域中的东西除了title之外通通被屏蔽。

    上面的例子中,scope参数使用了=号来指定获取属性的类型为父作用域的属性,如果我们想在指令中使用父作用域中的函数,使用&符号即可,是同样的原理。

    6.2.6指令间通信参数:controllerrequire

      使用指令来定义一个ui组件是个不错的想法,首先使用起来方便,只需要一个标签或者属性就可以了,其次是可复用性高,通过controller可以动态控制ui组件的内容,而且拥有双向绑定的能力。当我们想做的组件稍微复杂一点,就不是一个指令可以搞定的了,就需要指令与指令的协作才可以完成,这就需要进行指令间通信。

    想一下我们进行模块化开发的时候的原理,一个模块暴露(exports)对外的接口,另外一个模块引用(require)它,便可以使用它所提供的服务了。ng的指令间协作也是这个原理,这也正是自定义指令时controller参数和require参数的作用。

    controller参数用于定义指令对外提供的接口,它的写法如下:

     

    controller: function controllerConstructor($scope, $element, $attrs, $transclude)  

    它是一个构造器函数,将来可以构造出一个实例传给引用它的指令。为什么叫controller(控制器)呢?其实就是告诉引用它的指令,你可以控制我。至于可以控制那些东西呢,就需要在函数体中进行定义了。先看controller可以使用的参数,作用域、节点、节点的属性、节点内容的迁移,这些都可以通过依赖注入被传进来,所以你可以根据需要只写要用的参数。关于如何对外暴露接口,我们在下面的例子来说明。

    require参数便是用来指明需要依赖的其他指令,它的值是一个字符串,就是所依赖的指令的名字,这样框架就能按照你指定的名字来从对应的指令上面寻找定义好的controller了。不过还稍稍有点特别的地方,为了让框架寻找的时候更轻松些,我们可以在名字前面加个小小的前缀:^,表示从父节点上寻找,使用起来像这样:require : ‘^directiveName’,如果不加,$compile服务只会从节点本身寻找。另外还可以使用前缀:?,此前缀将告诉$compile服务,如果所需的controller没找到,不要抛出异常。

    所需要了解的知识点就这些,接下来是例子时间,依旧是从书上抄来的一个例子,我们要做的是一个手风琴菜单,就是多个折叠菜单并列在一起,此例子用来展示指令间的通信再合适不过。

    首先我们需要定义外层的一个结构,起名为accordion,代码如下:

    app.directive('accordion',function(){

            return {

                restrict : 'E',

                template : '<div ng-transclude></div>',

                replace : true,

                transclude : true,

    controller :function(){

                    var expanders = [];

                    this.gotOpended = function(selectedExpander){

                        angular.forEach(expanders,function(e){

                            if(selectedExpander != e){

                                e.showText = false;

                            }

                        });

                    }

                    this.addExpander = function(e){

                        expanders.push(e);

                    }

                }

            }

        });

    需要解释的只有controller中的代码,我们定义了一个折叠菜单数组expanders,并且通过this关键字来对外暴露接口,提供两个方法。gotOpended接受一个selectExpander参数用来修改数组中对应expander的showText属性值,从而实现对各个子菜单的显隐控制。addExpander方法对外提供向expanders数组增加元素的接口,这样在子菜单的指令中,便可以调用它把自身加入到accordion中。

    看一下我们的expander需要做怎样的修改呢:

    app.directive('expander',function(){

            return {

                restrict : 'E',

                templateUrl : 'expanderTemp.html',

                replace : true,

                transclude : true,

                require : '^?accordion',

                scope : {

                    title : '=etitle'

                },

     

                link : function(scope,element,attris,accordionController){

                    scope.showText = false;

                    accordionController.addExpander(scope);

                    scope.toggleText = function(){

                        scope.showText = ! scope.showText;

                        accordionController.gotOpended(scope);

                    }

                }

            };

        });

    首先使用require参数引入所需的accordion指令,添加?^前缀表示从父节点查找并且失败后不抛出异常。然后便可以在link函数中使用已经注入好的accordionController了,调用addExpander方法将自己的作用域作为参数传入,以供accordionController访问其属性。然

    后在toggleText方法中,除了要把自己的showText修改以外,还要调用accordionController的gotOpended方法通知父层指令把其他菜单给收缩起来。

    指令定义好后,我们就可以使用了,使用起来如下:

     

    <accordion>

    <expander ng-repeat="expander in expanders" etitle="expander.title"> 

    {{expander.text}} 

    </expander>

    </accordion>  

    外层使用了accordion指令,内层使用expander指令,并且在expander上用ng-repeat循环输出子菜单。请注意这里遍历的数组expanders可不是accordion中定义的那个expanders,如果你这么认为了,说明还是对作用域不够了解。此expanders是ng-repeat的值,它是在外层controller中的,所以,在testC中,我们需要添加如下数据:

    $scope.expanders = [

                {title: '个人简介',

                 text: '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流'},

                {title: '我的爱好',

                 text: 'LOL '},

                {title: '性格',

                 text: ' 我的性格就是无性格'}

            ];

    6.3 性能及调优

    6.3.1性能测试

    AnglarJS作为一款优秀的Web框架,可大大简化前端开发的负担。

    AnglarJS很棒,但当处理包含复杂数据结构的大型列表时,其运行速度就会非常慢。

    这是我们将核心管理页面迁移到AngularJS过程中遇到的问题。这些页面在显示500行数据时本应该工作顺畅,但首个方法的渲染时间竟花费了7秒,太可怕了。后来,我们发现了在实现过程中存在两个主要性能问题。一个与“ng-repeat ”指令有关,另一个与过滤器有关。

    AngularJS 中的ng-repeat在处理大型列表时,速度为什么会变慢? 

    AngularJS中的ng-repeat在处理2500个以上的双向数据绑定时速度会变慢。这是由于AngularJS通过“dirty checking”函数来检测变化。每次检测都会花费时间,所以包含复杂数据结构的大型列表将降低你应用的运行速度。

    提高性能的先决条件 

    时间记录指令 

    为了测量一个列表渲染所花费的时间,我们写了一个简单的程序,通过使用“ng-repeat”的属性“$last”来记录时间。时间存放在TimeTracker服务中,这样时间记录就与服务器端的数据加载分开了。

    // Post repeat directive for logging the rendering time   

    angular.module('siApp.services').directive('postRepeatDirective',   

    ['$timeout', '$log',  'TimeTracker',   

      function($timeout, $log, TimeTracker) {  

        return function(scope, element, attrs) {  

          if (scope.$last){  

             $timeout(function(){  

                 var timeFinishedLoadingList = TimeTracker.reviewListLoaded();  

                 var ref = new Date(timeFinishedLoadingList);  

                 var end = new Date();  

                 $log.debug("## DOM rendering list took: " + (end - ref) + " ms");  

             });  

           }  

        };  

      }  

    ]);  

    // Use in HTML:   

    <tr ng-repeat="item in items" post-repeat-directive>…</tr>  

    Chrome开发者工具的时间轴(Timeline)属性 

    在Chrome开发者工具的时间轴标签中,你可以看见事件、每秒内浏览器帧数和内存分配。“memory”工具用来检测内存泄漏,及页面所需的内存。当帧速率每秒低于30帧时就会出现页面闪烁问题。“frames”工具可帮助了解渲染性能,还可显示出一个JavaScript任务所花费的CPU时间。

    通过限制列表的大小进行基本的调优 

    缓解该问题,最好的办法是限制所显示列表的大小。可通过分页、添加无限滚动条来实现。

    分页,我们可以使用AngularJS的“limitTo”过滤器(AngularJS1.1.4版本以后)和“startFrom”过滤器。可以通过限制显示列表的大小来减少渲染时间。这是减少渲染时间最高效的方法。

    6.3.2七大调优法则 

    1.渲染没有数据绑定的列表 

    这是最明显的解决方案,因为数据绑定是性能问题最可能的根源。如果你只想显示一次列表,并不需要更新、改变数据,放弃数据绑定是绝佳的办法。不过可惜的是,你会失去对数据的控制权,但除了该法,我们别无选择。

    2.不要使用内联方法计算数据 

    为了在控制器中直接过滤列表,不要使用可获得过滤链接的方法。“ng-repeat”会评估每个表达式。在我们的案例中,“filteredItems()”返回过滤链接。如果评估过程很慢,它将迅速降低整个应用的速度。

     

    l <li ng-repeat="item in filteredItems()"> //这并不是一个好方法,因为要频繁地评估。   

    l <li ng-repeat="item in items"> //这是要采用的方法  

    3.使用两个列表(一个用来进行视图显示,一个作为数据源) 

    将要显示的列表与总的数据列表分开,是非常有用的模型。你可以对一些过滤进行预处理,并将存于缓存中的链接应用到视图上。下面案例展示了基本实现过程。filteredLists变量保存着缓存中的链接,applyFilter方法来处理映射。

    /* Controller */  

    // Basic list    

    var items = [{name:"John", active:true }, {name:"Adam"}, {name:"Chris"}, {name:"Heather"}];    

    // Init displayedList   

    $scope.displayedItems = items;  

    // Filter Cache   

    var filteredLists['active'] = $filter('filter)(items, {"active" : true});  

    // Apply the filter   

    $scope.applyFilter = function(type) {  

        if (filteredLists.hasOwnProperty(type){ // Check if filter is cached   

           $scope.displayedItems = filteredLists[type];  

        } else {   

            /* Non cached filtering */  

        }  

    }  

    // Reset filter   

    $scope.resetFilter = function() {  

        $scope.displayedItems = items;  

    }  

    /* View */  

    <button ng-click="applyFilter('active')">Select active</button>  

    <ul><li ng-repeat="item in displayedItems">{{item.name}}<li></ul>  

    4.在其他模板中使用ng-if来代替ng-show 

    如果你用指令、模板来渲染额外的信息,例如通过点击来显示列表项的详细信息,一定要使用  ng-if(AngularJSv. 1.1.5以后)。ng-if可阻止渲染(与ng-show相比)。所以其它DOM和数据绑定可根据需要进行评估。

    <li ng-repeat="item in items">  

     

       <p> {{ item.title }} </p>  

       <button ng-click="item.showDetails = !item.showDetails">Show details</buttons>  

       <div ng-if="item.showDetails">  

           {{item.details}}  

       </div>  

    </li>  

    5.不要使用ng-mouseenter、ng-mouseleave等指令 

    使用内部指令,像ng-mouseenter,AngularJS会使你的页面闪烁。浏览器的帧速率通常低于每秒30帧。使用jQuery创建动画、鼠标悬浮效果可以解决该问题。确保将鼠标事件放入jQuery的.live()函数中。

    6.关于过滤的小提示:通过ng-show隐藏多余的元素 

    对于长列表,使用过滤同样会减低工作效率,因为每个过滤都会创建一个原始列表的子链接。在很多情况下,数据没有变化,过滤结果也会保持不变。所以对数据列表进行预过滤,并根据情况将它应用到视图中,会大大节约处理时间。

    在ng-repeat指令中使用过滤器,每个过滤器会返回一个原始链接的子集。AngularJS 从DOM中移除多余元素(通过调用 $destroy),同时也会从$scope中移除他们。当过滤器的输入发生改变时,子集也会随着变化,元素必须进行重新链接,或着再调用$destroy。

    大部分情况下,这样做很好,但一旦用户经常过滤,或者列表非常巨大,不断的链接与

    销毁将影响性能。为了加快过滤的速度,你可以使用ng-show和ng-hide指令。在控制器中,进行过滤,并为每项添加一个属性。依靠该属性来触发ng-show。结果是,只为这些元素增加ng-hide类,来代替将它们移除子列表、$scope和DOM。

    触发ng-show的方法之一是使用表达式语法。ng-show的值由表达式语法来确定。可以看下面的例子:

    <input ng-model="query"></input>  

    <li ng-repeat="item in items" ng-show="([item.name] | filter:query).length"> {{item.name}} </li>

    <span style="font-size: 14px; line-height: 24px; font-family:; white-space: normal;"></span> 

    7.关于过滤的小提示:防抖动输入

    解决第6点提出的持续过滤问题的另一个方法是防抖动用户输入。例如,如果用户输入一个搜索关键词,只当用户停止输入后,过滤器才会被激活。使用该防抖动服务的一个很好的解决方案请见: http://jsfiddle.net/Warspawn/6K7Kd/。将它应用到你的视图及控制器中,如下所示:

    /* Controller */  

    // Watch the queryInput and debounce the filtering by 350 ms.   

    $scope.$watch('queryInput', function(newValue, oldValue) {  

        if (newValue === oldValue) { return; }  

        $debounce(applyQuery, 350);  

    });  

    var applyQuery = function() {   

        $scope.filter.query = $scope.query;  

    };    

    /* View */  

    <input ng-model="queryInput"/>  

    <li ng-repeat= item in items | filter:filter.query>{{ item.title }} </li> 

     

  • 相关阅读:
    敏捷不是XP(口水文)
    利用异或的特性解决,找出重复数的问题,应该是目前最优算法。
    开源和免费那些事儿(二)
    开源和免费那些事儿
    LINQ本质 外篇 JOIN补遗
    在北京求.NET开发职位,人已经到达北京
    软件是邪恶的
    最近遇到的两个面试题兼卖身广告
    谈谈信仰和银弹。
    继续高阶函数好玩有用的扩展(网吧行文)
  • 原文地址:https://www.cnblogs.com/ndos/p/8331810.html
Copyright © 2011-2022 走看看