这篇文章主要是面向那些刚开始学AngularJs和想要了解数据绑定(data-binding)是怎么工做的,javascript
若是你已经熟悉如何使用angularjs了,我强烈建议你不用阅读了。html
angularjs使用者想要知道data-binding是如何工做的,就会遇到不少的关的术语java
好比$wacth,$apply,$digest,dirty-checking(脏值检测)...等等,这些又是作什么的呢?angularjs
在这篇文章里我会解决全部的疑问,经过结合这些术语在一块儿来学习。数组
可是我会尽可能用简单的方式来讲明。浏览器
如今开始app
咱们的浏览器会检测等待事件发生,好比用户的一些行为,假如你点击了一个button或者在input写东西,函数
事件的回调就会在内置的JavaScript跑起来,而后你就可以作一些DOM操做了。oop
因此当回调发生的时候,浏览器中的DOM会发生一些变化。性能
而Angularjs扩展了这个事件轮询,建立了一个叫angular content的东西(记住它,很是重要的一个概念),
为了解释这个context是什么以及它是怎么工做的,咱们须要先了解一下其余的一些概念。
每当你在ui上绑定了东西,就会添加了一个$wacth到$watch list中
你能够把$watch想象成为一个可以察觉model的变化的检测器,
好比你的html代码是
User: <input type="text" ng-model="user" /> Password: <input type="password" ng-model="pass" />
在这里,咱们将$scope.user绑定到了第一个input上,把$scope.pass绑定到了第二个input上,
这样就意味着,咱们添加了两个$wacth到了$watch list中了。
再好比
app.controller('MainCtrl', function($scope) { $scope.foo = "Foo"; $scope.world = "World"; });
//页面htnl Hello, {{ World }}
这个例子中,虽然咱们在控制器中定义了foo和world,可是只有一个绑定到了页面上,
因此这个例子中,咱们只建立了一个$watch。
咱们再来看看下面这种状况:
app.controller('MainCtrl', function($scope) { $scope.people = [...]; }); //HTML代码 <ul> <li ng-repeat="person in people"> {{person.name}} - {{person.age}} </li> </ul>
那这个例子有多少个$watch被建立呢?
咱们在这里假设peple数组中的每一个元素都拥有name和age这2个属性。
因为咱们实用ng-repeat遍历$scope.people,
若是有10我的,则咱们一共建立了10*2+1 = 22
注意:其中的1,为ng-repeat所建立的。
因此要注意的是,当咱们在ui上使用(绑定)用directives 指令的时候,就建立了一个$watch,
对了,那它是何时建立的呢?
当咱们的模板加载完成(亦叫作linking phase),compiler就会找到全部的指令,并建立对应的$watch。
还记得咱们在上面讨论的event-loop吗?
当浏览器发送一个事件,咱们就能经过angular context管理这个事件,此时$digest就会被激活
这个事件轮询loop由两个小loop组成,
一个是处理$evalAsync队列
一个是处理$watch list,这就是本文的主题了。
那处理的过程是怎样的?$digest会轮询咱们有的$watch list,大概就想下面这样
---》Hey,$watch,你的value值是多少啊?
---》个人value值是9。
---》好的,有变化吗?
---》没有。
(什么都没有变化,就会跳到下个继续询问)
---》来,你的value值是多少啊?
---》是foo。
---》有变化吗?
---》有啊,原本是bar的
(很好,那么如今咱们有一个DOM要更新下了)
流程就会这样继续下去,知道$watch list中全部的$watch都被询问...
如今讲下脏值检测(dirty-checking),如今全部的$watch都被轮询过了,
会再次询问是否全部的$watch都已经更新了?
若是其中有一个改变了,就会从新轮询,直到全部的$wacth没有改变。这样作事为了确保全部的model都是“干净的”
注意:若是轮询loop超过十次,就会抛出异常,来退出无限的轮询。
当$degest loop完成,DOM就会发生改变
看下面的例子
app.controller('MainCtrl', function() { $scope.name = "Foo"; $scope.changeFoo = function() { $scope.name = "Bar"; } });
{{ name }}
<button ng-click="changeFoo()">Change the name</button>
这里咱们只有一个$watch,,由于ng-click不会建立任何的$watch(函数function是不会发生改变的)
1·当咱们点击按钮的时候
2·浏览器发送事件,进入angular context(后面再解释这个context)
3·而后执行$degest loop会轮询全部的$watch是否发生改变
4·新的一轮loop报告说没有新的改变了
5·而后浏览器就会拿回控制权,而后更新DOM,这里是更新新的值,$scope.name
这里最重要的一点(也是难点)是全部的事件会进入angular context而后执行$digest loop
这意味着每当咱们在input键入一个字符,轮询loop会检查页面上全部的$watch。
那是怎么进入angular context的呢?答案是$apply
当一个事件发生时吗,你若是调用$apply,就会进入angular context。
若是你没有调用,就只会在外部(这里指的是区别于angular context内部)执行
那么你就可能会有疑问了,
上面的那个例子我没有调用$apply,它为何会进入angular context呢?答案是Angular帮咱们作了。
全部当你调用ng-click时候,这个事件就会被包含进了$apply调用了。
若是你在一个页面上有input,而且标签上写着ng-model="foo",
而后输入“f”,事件就像这样$apply("foo = 'f';")被调用,换句话说,就是被$apply包含着调用了。
这是不少Angular的新手共同的疑问,我在页面上上使用了jQuery,为何JQuery没有更新个人绑定呢?
由于jQuery没有调用$apply,全部的事件并无进入angular context中,因此$digest loop没有执行
下面让咱们看看一个有趣的例子:
假如咱们的代码中有下面的指令directive 和控制器controller
app.directive('clickable', function() { return { restrict: "E", scope: { foo: '=', bar: '=' }, template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>', link: function(scope, element, attrs) { element.bind('click', function() { scope.foo++; scope.bar++; }); } } }); app.controller('MainCtrl', function($scope) { $scope.foo = 0; $scope.bar = 0; });
例子中咱们绑定了foo和bar到了ul上的li中,咱们想每当咱们点击的时候,foo和bar都会增长一
那实际状况中咱们点击以后会发生什么呢?咱们能不能看到foo和bar按照咱们预期的变化呢?
答案是不会,由于上面的click是没有被包含在$apply中调用的。
这样是否意味着咱们不能这样控制咱们想要的变化呢?实际上不是的,咱们是能够的
实际上$scope上的foo和bar都是变化了的,只是它没有执行$digest loop,因此也就没有意识到$watch的变化
这样也意味着,若是我在以后调用$apply,这样全部的$watch都会知道变化,而后就会更新相应的DOM
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="http:////cdn.staticfile.org/angular.js/1.2.10/angular.min.js"></script>
<meta charset=utf-8 />
<title>Directive example</title>
</head>
<body ng-controller="MainCtrl">
<clickable foo="foo" bar="bar"></clickable>
<hr />
{{ hello }} <button ng-click="setHello()">Change hello</button>
</body>
</html>
app = angular.module('app', []); app.controller('MainCtrl', function($scope) { $scope.foo = 0; $scope.bar = 0; $scope.hello = "Hello"; $scope.setHello = function() { $scope.hello = "World"; }; }); app.directive('clickable', function() { return { restrict: "E", scope: { foo: '=', bar: '=' }, template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>', link: function(scope, element, attrs) { element.bind('click', function() { scope.foo++; scope.bar++; }); } } });
若是你点击上面蓝色区域,你是不会看到任何变化的,可是以后再点击button的话,
你就会忽然看到0的数字变成一个数字,这个数字就是刚刚你点击了蓝色区域多少下的数字
就像刚刚我上面说的那样,指令里的click并无触发$digest loop,可是当你点击button按钮的时候
按钮button上的ng-click就会调用$apply,而后就会执行$digest loop,
全部的$watch就会检查有没有改变(其中包含了foo和bar这两个$watch)
这时,你可能会想,这样的结果不是你想要的,你想要你点击那个指令块的时候立刻就能看到变化。
其实这很简单,只须要调用的时候包含$apply就能够了
element.bind('click', function() { scope.foo++; scope.bar++; scope.$apply(); });
$apply是$scope(或者是指令link function上的scope)上的一个函数function,因此调用它会强制执行$digest loop
注意,若是已经有一个轮询loop了,在这种状况下调用它将抛出一个异常,这是一个迹象代表,咱们不须要再调用$apply了。
其实上面的用法更好的是这样使用
element.bind('click', function() { scope.$apply(function() { scope.foo++; scope.bar++; }); })
这两种用法有什么区别呢?
区别是第一种用法中,咱们更新新的值是在angular context外部,因此当有错误的时候,Angular是不会知道的。
显然在上面的小例子中它不会产生多大影响,可是想像下当咱们在复杂项目使用多种库,而后出错的时候,Angular是不会知道本身的错误
因此若是你想在在项目中使用 jQuery plugin,你要确保你有调用$apply来执行$degest loop来更新DOM的变化。
最后,我但愿你看完就明白了 Angular中data-binding的工做原理,我猜你看完的第一印象会以为dirty-checking是很慢的
实际上它是很快的,实际上只有页面上达到2000-3000 个$watch的时候,它才会出现性能上的延迟,可是我以为你应该用不到那么多个。
注:本文基本取自于$watch How the $apply Runs a $digest
原文地址http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/