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>

Angular进阶教程二的更多相关文章

  1. Android高手进阶教程(二十八)之---Android ViewPager控件的使用(基于ViewPager的横向相册)!!!

      分类: Android高手进阶 Android基础教程 2012-09-14 18:10 29759人阅读 评论(35) 收藏 举报 android相册layoutobjectclassloade ...

  2. SpringBoot进阶教程(二十九)整合Redis 发布订阅

    SUBSCRIBE, UNSUBSCRIBE 和 PUBLISH 实现了 发布/订阅消息范例,发送者 (publishers) 不用编程就可以向特定的接受者发送消息 (subscribers). Ra ...

  3. SpringBoot进阶教程(二十七)整合Redis之分布式锁

    在之前的一篇文章(<Java分布式锁,搞懂分布式锁实现看这篇文章就对了>),已经介绍过几种java分布式锁,今天来个Redis分布式锁的demo.redis 现在已经成为系统缓存的必备组件 ...

  4. SpringBoot进阶教程(二十八)整合Redis事物

    Redis默认情况下,事务支持被禁用,必须通过设置setEnableTransactionSupport(true)为使用中的每个redistplate显式启用.这样做会强制将当前重新连接绑定到触发m ...

  5. SpringBoot进阶教程(二十六)整合Redis之共享Session

    集群现在越来越常见,当我们项目搭建了集群,就会产生session共享问题.因为session是保存在服务器上面的.那么解决这一问题,大致有三个方案,1.通过nginx的负载均衡其中一种ip绑定来实现( ...

  6. SpringBoot进阶教程(二十五)整合Redis之@Cacheable、@CachePut、@CacheEvict的应用

    在上一篇文章(<SpringBoot(二十四)整合Redis>)中,已经实现了Spring Boot对Redis的整合,既然已经讲到Cache了,今天就介绍介绍缓存注解.各家互联网产品现在 ...

  7. SpringBoot进阶教程(二十三)Linux部署Quartz

    在之前的一篇文章中<SpringBoot(九)定时任务Schedule>,已经详细介绍了关于schedule框架的配置和使用,有收到一些朋友关于部署的私信,所以抽时间整理一个linux部署 ...

  8. Angular进阶教程三

    7 总结 angular上手比较难,初学者(特别是习惯了使用JQuery的人)可能不太适应其语法以及思想.随着对ng探索的一步步深入,也确实感觉到了这一点,尤其是框架内部的某些执行机制. 7.1页面效 ...

  9. Angular入门教程二

    4 功能介绍 4.1数据绑定 AngularJS的双向数据绑定,意味着你可以在Mode(JS)中改变数据,而这些变动立刻就会自动出现在View上,反之亦然.即:一方面可以做到model变化驱动了DOM ...

随机推荐

  1. 题目1003:A+B(字符串转数字)

    问题来源 http://ac.jobdu.com/problem.php?pid=1003 问题描述 每次给你两个数,数的形式是每三位有一个间隔符',',计算两数之和. 问题分析 两个问题,一.如何读 ...

  2. c#-MVC基础操作-数据的展示及增删改、登录页面及状态保持

    一.数据展示 1.View代码: <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynam ...

  3. 最新版chrome浏览器如何离线安装crx插件?(转载)

    原文链接:https://newsn.net/say/chrome-crx-offline.html mac新版chrome开启离线插件安装 对于mac新版chrome,注意,大家一定要按照顺序来.m ...

  4. CentOS7 安装 Adobe Flash 看网络视频

    登录 Adobe 网站,找到 Adobe Flash Player 下载页,进入后网页自动识别 Linux 环境,手动选择版本,选择 YUM 方式,自动下载一个 rpm 文件 定位到下载目录,通常默认 ...

  5. WC2019退役记

    sb题不会,暴力写不完,被全场吊着打,AFO

  6. 插播一条 QQ头像无法正常显示问题

    问题背景 不知道啥什么,QQ群的头像有些显示不全直接是默认的头像.想一想最近也没做啥,怎么就出问题了. 后来想一想,大概是个人文件夹的文件出问题了 解决办法 好友头像显示问题的删除 MiscHead. ...

  7. remote link Centos6.6 Horrible Slow

    客户端win7 , 本地直连,secureCRT连接Centos6.6 速度巨慢,FTP tool almost cannot link in. 即使用cmd ftp 也是反应30s以上.

  8. Hadoop2.5.0伪分布式环境搭建

    本章主要介绍下在Linux系统下的Hadoop2.5.0伪分布式环境搭建步骤.首先要搭建Hadoop伪分布式环境,需要完成一些前置依赖工作,包括创建用户.安装JDK.关闭防火墙等. 一.创建hadoo ...

  9. Java 并发编程——Callable+Future+FutureTask

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  10. ubuntu设置root权限默认密码

    1.默认root密码是随机的,即每次开机都有一个新的root密码.我们可以在终端输入命令 sudo passwd,然后输入当前用户的密码2.终端会提示我们输入新的密码并确认,此时的密码就是root新密 ...