首页 > 技术文章 > angularjs 面试(一)

YangqinCao 2016-08-15 22:43 原文

1、angular的数据绑定采用什么机制?详述原理

angular进行双向数据绑定主要是他的脏检查机制。只负责对发生于AngularJS上下文环境中的变更会做出自动地响应, 在里面触发进入angular的digest流程,进入$digest cycle:

  • DOM事件,譬如用户输入文本,点击按钮等。(ng-click)
  • XHR响应事件 ($http)
  • 浏览器Location变更事件 ($location)
  • Timer事件($timeout, $interval)
  • 执行$digest()或$apply()
1)当你往UI界面上绑定数据时,angular就会把它加入到$watch队列
2)当你绑定的事件触发时,就会触发$apply,apply会调用$rootScope.$digest(),进入angular context,触发digest循环
3)当$digest循环开始后,它会检查watcher队列中的每个watcher。这些watchers会检查当前的值和之前的值是否一样,有没有发生改变,如果发生了改变,则调用回调函数,将新的值更新到界面上。
假设你将ng-click指令关联到了一个button上,并传入了一个function名到ng-click上。当该button被点击时,AngularJS会将此function包装到一个wrapping function中,然后传入到$scope.$apply()。因此,你的function会正常被执行,修改models(如果需要的话),此时一轮$digest循环也会被触发,用来确保view也会被更新。

$digest循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有models发生了变化。这就是脏检查(Dirty Checking),它用来处理在listener函数被执行时可能引起的model变化。因此,$digest循环会持续运行直到model不再发生变化,或者$digest循环的次数达到了10次。因此,尽可能地不要在listener函数中修改model。 

Note: $digest循环最少也会运行两次,即使在listener函数中并没有改变任何model。正如上面讨论的那样,它会多运行一次来确保models没有变化。

 

http://blog.csdn.net/dm_vincent/article/details/38705099
http://blog.csdn.net/dm_vincent/article/details/50344395

AngularJS中的$watch方法,这个方法很接近事件的注册和监听:

$scope.$watch(
    function(scope) { return scope.someValue; },
    function(newValue, oldValue, scope) { // listener code defined here }
);

$watch方法的第一个参数是一个函数,它通常被称为watch函数,它的返回值声明需要监听的变量;第二个参数是listener,也就是回调函数,在变量发生改变的时候会被调用。

$digest函数中,会逐个检查$watch方法中注册的watch函数,如果该函数返回的值和上一次检查中返回的值不一样的话,就会触发对应的listener函数。拿{{ }}表达式作为例子,该表达式编译得到的listener的行为就是将后台的最新变量给同步到前端。这么一来,就完成了一个简单的数据绑定。

$apply方法。这个方法能够触发$digest方法。$digest方法的执行就标志着一轮Digest Cycle的开始。

Note: $scope.$apply()会自动地调用$rootScope.$digest()$apply()方法有两种形式。第一种会接受一个function作为参数,执行该function并且触发一轮$digest循环。第二种会不接受任何参数,只是触发一轮$digest循环。

angularjs只负责对发生于AngularJS上下文环境中的变更会做出自动地响应(即,在$apply()方法中发生的对于models的更改)AngularJSbuilt-in指令就是这样做的,所以任何的model变更都会被反映到view中。但是,如果你在AngularJS上下文之外的任何地方修改了model,那么你就需要通过手动调用$apply()来通知AngularJS。这就像告诉AngularJS,你修改了一些models,希望AngularJS帮你触发watchers来做出正确的响应。

 比如,如果你使用了JavaScript中的setTimeout()来更新一个scope model,那么AngularJS就没有办法知道你更改了什么。这种情况下,调用$apply()就是你的责任了,通过调用它来触发一轮$digest循环。类似地,如果你有一个指令用来设置一个DOM事件listener并且在该listener中修改了一些models,那么你也需要通过手动调用$apply()来确保变更会被正确的反映到view中。

需要记住的是你总是应该使用接受一个function作为参数的$apply()方法。这是因为当你传入一个function$apply()中的时候,这个function会被包装到一个trycatch块中,所以一旦有异常发生,该异常会被$exceptionHandler service处理。

2、$apply()和 $digest()的区别

安全性:$apply()可以接收一个参数作为function(),这个 function 会被包装到一个 try … catch 块中,所以一旦有异常发生,该异常会被 $exceptionHandler service 处理。

  • $apply会使ng进入 $digest cycle , 并从$rootScope开始遍历(深度优先)检查数据变更。
  • $digest仅会检查该scope和它的子scope,当你确定当前操作仅影响它们时,用$digest可以稍微提升性能。

 

3、依赖注入

让我们可以不用自己实例化就能创建依赖对象的方法. 简单的来说, 依赖是以注入的方式传递的. 在Web应用中, Angular让我们可以通过DI来创建像Controllers和Directives这样的对象. 我们还可以创建自己的依赖对象, 当我们要实例化它们时, Angular能自动实现注入.

http://sentsin.com/web/663.html

provider服务

$provide服务负责告诉Angular如何创造一个新的可注入的东西:即服务(service)。服务会被叫做provider的东西来定义,你可以使用$provide来创建一个provider。你需要使用$provide中的provider方法来定义一个provider,同时你也可以通过要求改服务被注入到一个应用的config函数中来获得$provide服务。

app.config(function($provide) {
  $provide.provider('greeting', function() {
    this.$get = function() {
       return function(name) {
        alert("Hello, " + name);
       };
    };
  });
});  

 

在上面的例子中我们为一个服务定义了一个叫做greeting的新provider;我么可以把一个叫做greeting的变量注入到任何可注入的函数中(例如控制器,在后面会讲到)然后Angular就会调用这个provider的$get函数来返回这个服务的一个实例。在上面的例子中,被注入的是一个函数,它接受一个叫做name的参数并且根据这个参数alert一条信息。

factory,service以及value全部都是用来定义一个providr的简写,它们提供了一种方式来定义一个provider而无需输入所有的复杂的代码。

注入器($injector)

注入器负责从我们通过$provide创建的服务中创建注入的实例。每一个AngularJS应用都有唯一一个$injector,当应用启动的时候它被创造出来,你可以通过将$injector注入到任何可注入函数中来得到它($injector知道如何注入它自己!)。一旦你拥有了$injector,你可以动过调用get函数来获得任何一个已经被定义过的服务的实例。

var greeting = $injector.get('greeting');
greeting('Ford Prefect');  

注入器同样也负责将服务注入到函数中;例如,你可以魔法般的将服务注入到任何函数中,只要你使用了注入器的invoke方法:

var myFunction = function(greeting) {
  greeting('Ford Prefect');
};
$injector.invoke(myFunction);  

如果注入器只是创建一个服务的实例一次的话,那么它也没什么了不起的。它的厉害之处在于,他能够通过服务名称缓存从一个provider中返回的任何东西,当你下一次再使用这个服务时,你将会得到同一个对象。

因此,你可以通过调用$injector.invike将服务注入到任何函数中也是合情合理的了。包括:

  • 控制器定义函数
  • 指令定义函数
  • 过滤器定义函数
  • provider中的$get方法(也就是factory函数)

由于constant和value总是返回一个静态值,它们不会通过注入器被调用,因此你不能在其中注入任何东西。

在config阶段,只有provider能被注入(只有两个例外是$provide和$injector)。有一个例外:constant,由于它们不能被改变,因此它不能被注入到config中(这就是它和value之间的不同之处)。它们只能通过名字被获取。

控制器函数是可以被注入的,但是控制器本身是不能被注入到任何东西里面去的。这是因为控制器不是通过provider创建的。

filter和directive和controller的运行方式相同;filter会使用一个叫做$filter的服务以及它的provider $filterProvider,而directive使用一个叫做$compile的服务以及它的provider $compileProvidr。

 

4、有几种依赖注入的方式

1)简单注入方式

myModule.controller('myCtrl', myCtrl);

AngularJs会扫描function的参数,提取参数的名称(name)作为function的依赖,

所以这种方式要求保证参数名称的正确性,但对参数的顺序并没有要求;

但是这种注入方式有一个问题,当我们将项目发布到正式环境时都会压缩我们的代码,这时function的参数可能会变成a,b,这就会导致我们的代码出现问题,下面两种注入方式可以帮我们解决这个问题。

2)数组注释法

myModule.controller('myCtrl', ['$scope', 'Preject', function($scope, Project)

每一个依赖的参数值(字符串)都会以相同的顺序存放在一个数组里,数组的值与后面的function参数一一对应,这样即使压缩了也不会有什么问题。

 3)显示调用function的$inject

function myCtrl(a, b) {
    //$scope, Project,故意改成a,b模拟压缩后的情形
}
myCtrl.$inject = ['$scope', 'Project'];
myModule.controller('PhoneDetailCtrl', myCtrl);

通过设置funciton的$inject属性,可以达到依赖注入的效果;

 

5、directive中compile和link的区别

5.1 angular是怎么处理指令的

浏览器渲染一个页面时,本质上是读html标志,然后建立dom节点,当dom树创建完毕后,广播事件给我们。当使用script标签加载angular应用程序代码时,angular会监听上面的dom完成事件,查找带有ng-app的元素,然后以这个元素为起点,递归查找所有子元素里面符合应用程序定义好的指令规则。

ng怎样处理指令其实是依赖于它定义时的对象属性的,你可以定义一个compile或者一个link函数,或者用pre-link和post-link函数来代替link.

<level-one>
        <level-two>
            <level-three>
                Hello 
            </level-three>
        </level-two>
</level-one>

<script>
    function createDirective(name){
      return function(){
        return {
          restrict: 'E',
          compile: function(tElem, tAttrs){
            console.log(name + ': compile');
            return {
              pre: function(scope, iElem, iAttrs){
                console.log(name + ': pre link');
              },
              post: function(scope, iElem, iAttrs){
                console.log(name + ': post link');
              }
            }
          }
        }
      }
    }
    app.directive('levelOne', createDirective('levelOne'));
    app.directive('levelTwo', createDirective('levelTwo'));
    app.directive('levelThree', createDirective('levelThree'));
</script>

 

输出

levelOne: compile
levelTwo: compile
levelThree: compile

levelOne: pre link
levelTwo: pre link
levelThree: pre link

levelThree: post link    // post link执行顺序是逆序执行
levelTwo: post link
levelOne: post link

 

稍微修改下上一个例子

<level-one>
        <level-two>
            <level-three>
                Hello {{name}}
            </level-three>
        </level-two>
</level-one>
function createDirective(name){ 
      return function(){
        return {
          restrict: 'E',
          compile: function(tElem, tAttrs){
            console.log(name + ': compile => ' + tElem.html());
            return {
              pre: function(scope, iElem, iAttrs){
                console.log(name + ': pre link => ' + iElem.html());
              },
              post: function(scope, iElem, iAttrs){
                console.log(name + ': post link => ' + iElem.html());
              }
            }
          }
        }
      }
    }
    app.directive('levelOne', createDirective('levelOne'));
    app.directive('levelTwo', createDirective('levelTwo'));
    app.directive('levelThree', createDirective('levelThree'));

 

输出

levelOne: compile => 
        <level-two>
            <level-three>
                Hello {{name}}
            </level-three>
        </level-two>
levelTwo: compile => 
            <level-three>
                Hello {{name}}
            </level-three>
levelThree: compile => 
                Hello {{name}}
levelOne: pre link => 
        <level-two>
            <level-three class="ng-binding">
                Hello {{name}}
            </level-three>
        </level-two>
levelTwo: pre link => 
            <level-three class="ng-binding">
                Hello {{name}}
            </level-three>
levelThree: pre link => 
                Hello {{name}}
            
levelThree: post link => 
                Hello {{name}}
levelTwo: post link => 
            <level-three class="ng-binding">
                Hello {{name}}
            </level-three>
levelOne: post link => 
        <level-two>
            <level-three class="ng-binding">
                Hello {{name}}
            </level-three>
        </level-two>

 

 

我们已经知道当ng发现dom构建完成时就开始处理dom.所以当angular遍历dom时碰到level-one元素,从它的定义了解到要执行一些函数,因为compile函数定义在level-one指令的指令对象里,所以它会被调用并传递一个element对象作为它的参数,浏览器在创建这个element对象时,任然是最原始的html标记,用以标志template element,一旦运行levelone指令中的compile函数,就会递归深度遍历它的dom节点,然后在level-two level-three上面重复这些操作。

对于post-link,如果在定义指令时只使用了一个Link函数,默认把它当做post-link。

 

当运行包含子指令的指令post-link时,反向的post-link规则可以保证它的子指令的post-link是已经运行过的.所以,当运行level-one指令的post-link函数的时候,我们能够保证level-two和level-three的post-link其实都已经运行过了.这就是为什么人们都认为post-link是最安全或者默认的写业务逻辑的地方.

但是为什么这里的element跟compile里的又不同呢?

一旦ng调用过指令的compile函数,就会创建一个template element的element实例对象,并且为它提供一个scope对象,这个scope有可能是新实例,也有可能是已经存在,可能是个子scope,也有可能是独立的scope,这些都得依赖指令定义对象里的scope属性值,所以当linking发生时,这个实例element以及scope对象已经是可用的了,并且被ng作为参数传递到post-link函数的参数列表中去.

所以post-link(pre-link)函数的element参数对象是一个element实例而不是一个template element.

所以上面例子里的输出是不同的

pre-link

当写了一个post-link函数,你可以保证在执行post-link函数的时候,它的所有子级指令的post-link函数是已经执行过的.

在大部分的情况下,它都可以做的更好,因此通常我们都会使用它来编写指令代码.

然而,ng为我们提供了一个附加的hook机制,那就是pre-link函数,它能够保证在执行所有子指令的post-link函数之前.运行一些别的代码.

一个元素的pre-link函数能够保证是运行在它所有的子指令的post-link与pre-link运行之前执行的

 

总结:

compile

使用compile函数可以改变原始的dom(template element),在ng创建原始dom实例以及创建scope实例之前.

可以应用于当需要生成多个element实例,只有一个template element的情况,ng-repeat就是一个最好的例子,它就在是compile函数阶段改变原始的dom生成多个原始dom节点,然后每个又生成element实例.因为compile只会运行一次,所以当你需要生成多个element实例的时候是可以提高性能的.

template element以及相关的属性是做为参数传递给compile函数的,不过这时候scope是不能用的:

pre-link

使用pre-link函数可以运行一些业务代码在ng执行完compile函数之后,但是在它所有子指令的post-link函数将要执行之前.

scope对象以及element实例将会做为参数传递给pre-link函数:

post-link

使用post-link函数来执行业务逻辑,在这个阶段,它已经知道它所有的子指令已经编译完成并且pre-link以及post-link函数已经执行完成.

这就是被认为是最安全以及默认的编写业务逻辑代码的原因.

scope实例以及element实例做为参数传递给post-link函数:

转自:http://www.jb51.net/article/58229.htm

 

再看一个例子

<div autohello='5'>
        <p>HELLO</p>
</div>
<script>
app.directive('autohello',function(){
        return{
            restrict:'A',
            compile:function(ele,attrs){
                console.log('指令编译');
                var tpl=ele.children().clone();
                console.log(tpl);
                for(var i=0;i<attrs.autohello-1;i++){
                    ele.append(tpl.clone());
                }
                return function(scope,ele,attrs){   // 这里return的其实就是link函数,写compile必须返回该link函数
                  var str=ele.children().html();
                    ele.children().html(str+' world');
                    console.log('指令链接');
                }
            }
        }
    })
</script>

 

 

输出5个HELLO world

compile中将模板定好了,是5个hello,然后在link中在每个后面加上world

在最后return一个闭包函数,其实就是我们的所说的link函数。

 

那么可以写多个Link函数吗

compile:function(){
},
link:function(){
}
 

 

后面这个link已经不起作用了,当同时设置了两个选项,那么会把compile所返回的函数当做link函数,而link选项本身则会被忽略。如果注释掉compile,link就起作用了。

compile函数的作用是对指令的模板进行转换。

link是在模型和视图之间建立关联,包括在元素上注册事件监听。

scope在link阶段才会被绑定在元素上,compile阶段操作scope会报错。

对同一个指令的多个实例,compile只会执行一次,而link对于指令的每个实例都会执行一次。

一般情况只要编写link就好了。

如果你自定义了compile,那么自定义的link就无效了,因为compile返回了一个link函数做后续处理。 

 

推荐阅读