1、angular的数据绑定采用什么机制?详述原理
angular进行双向数据绑定主要是他的脏检查机制。只负责对发生于AngularJS上下文环境中的变更会做出自动地响应, 在里面触发进入angular的digest流程,进入$digest cycle:
- DOM事件,譬如用户输入文本,点击按钮等。(ng-click)
- XHR响应事件 ($http)
- 浏览器Location变更事件 ($location)
- Timer事件($timeout, $interval)
- 执行$digest()或$apply()
$digest循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有models发生了变化。这就是脏检查(Dirty Checking),它用来处理在listener函数被执行时可能引起的model变化。因此,$digest循环会持续运行直到model不再发生变化,或者$digest循环的次数达到了10次。因此,尽可能地不要在listener函数中修改model。
Note: $digest循环最少也会运行两次,即使在listener函数中并没有改变任何model。正如上面讨论的那样,它会多运行一次来确保models没有变化。
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的更改)。AngularJS的built-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会被包装到一个try…catch块中,所以一旦有异常发生,该异常会被$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函数做后续处理。