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> 

 

posted @ 2016-07-14 00:29  nDos  阅读(420)  评论(0编辑  收藏  举报