$apply()和$digest()在AngularJS中是两个核心概念,可是有时候它们又让人困惑。而为了了解AngularJS的工做方式,首先须要了解$apply()和$digest()是如何工做的。这篇文章旨在解释$apply()和$digest()是什么,以及在平常的编码中如何应用它们。app
探索$apply()和$digest()函数
AngularJS提供了一个很是酷的特性叫作双向数据绑定(Two-way Data Binding),这个特性大大简化了咱们的代码编写方式。数据绑定意味着当View中有任何数据发生了变化,那么这个变化也会自动地反馈到scope的数据上,也即意味着scope模型会自动地更新。相似地,当scope模型发生变化时,view中的数据也会更新到最新的值。那么AngularJS是如何作到这一点的呢?当你写下表达式如{{ aModel }}时,AngularJS在幕后会为你在scope模型上设置一个watcher,它用来在数据发生变化的时候更新view。这里的watcher和你会在AngularJS中设置的watcher是同样的:ui
$scope.$watch(‘aModel’, function(newValue, oldValue) { //update the DOM with newValue });
传入到$watch()中的第二个参数是一个回调函数,该函数在aModel的值发生变化的时候会被调用。当aModel发生变化的时候,这个回调函数会被调用来更新view,这一点不难理解,可是,还存在一个很重要的问题!AngularJS是如何知道何时要调用这个回调函数呢?换句话说,AngularJS是如何知晓aModel发生了变化,才调用了对应的回调函数呢?它会周期性的运行一个函数来检查scope模型中的数据是否发生了变化吗?好吧,这就是$digest循环的用武之地了。this
在$digest循环中,watchers会被触发。当一个watcher被触发时,AngularJS会检测scope模型,若是它发生了变化那么关联到该watcher的回调函数就会被调用。OK,下一个问题就是$digest循环是在何时以各类方式开始的?编码
在调用了$scope.$digest()后,$digest循环就开始了。假设你在一个ng-click指令对应的handler函数中更改了scope中的一条数据,此时AngularJS会自动地经过调用$digest()来触发一轮$digest循环。当$digest循环开始后,它会触发每一个watcher。这些watchers会检查scope中的当前model值是否和上一次计算获得的model值不一样。若是不一样,那么对应的回调函数会被执行。调用该函数的结果,就是view中的表达式内容(译注:诸如{{ aModel }})会被更新。除了ng-click指令,还有一些其它的built-in指令以及服务来让你更改models(好比ng-model,$timeout等)和自动触发一次$digest循环。code
目前为止还不错!可是,有一个小问题。在上面的例子中,AngularJS并不直接调用$digest(),而是调用$scope.$apply(),后者会调用$rootScope.$digest()。所以,一轮$digest循环在$rootScope开始,随后会访问到全部的children scope中的watchers。事件
如今,假设你将ng-click指令关联到了一个button上,并传入了一个function名到ng-click上。当该button被点击时,AngularJS会将此function包装到一个wrapping function中,而后传入到$scope.$apply()。所以,你的function会正常被执行,修改models(若是须要的话),此时一轮$digest循环也会被触发,用来确保view也会被更新。ip
Note: $scope.$apply()会自动地调用$rootScope.$digest()。$apply()方法有两种形式。第一种会接受一个function做为参数,执行该function而且触发一轮$digest循环。第二种会不接受任何参数,只是触发一轮$digest循环。咱们立刻会看到为何第一种形式更好。get
何时手动调用$apply()方法?回调函数
若是AngularJS老是将咱们的代码wrap到一个function中并传入$apply(),以此来开始一轮$digest循环,那么何时才须要咱们手动地调用$apply()方法呢?实际上,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中。
让咱们来看一个例子。加入你有一个页面,一旦该页面加载完毕了,你但愿在两秒钟以后显示一条信息。你的实现多是下面这个样子的:
<body ng-app=“myApp”> <div ng-controller=“MessageController”> Delayed Message: {{message}} </div> </body>
/* What happens without an $apply() */ angular.module(‘myApp’,[]) .controller(‘MessageController’, function($scope) { $scope.getMessage = function() { setTimeout(function() { $scope.message = ‘Fetched after 3 seconds'; console.log(‘message:’+$scope.message); }, 2000); } $scope.getMessage(); });
经过运行这个例子,你会看到过了两秒钟以后,控制台确实会显示出已经更新的model,然而,view并无更新。缘由也许你已经知道了,就是咱们忘了调用$apply()方法。所以,咱们须要修改getMessage(),以下所示:
/* What happens with $apply */ angular.module(‘myApp’,[]).controller(‘MessageController’, function($scope) { $scope.getMessage = function() { setTimeout(function() { $scope.$apply(function() { //wrapped this within $apply $scope.message = ‘Fetched after 3 seconds'; console.log(‘message:’ + $scope.message); }); }, 2000); } $scope.getMessage(); });
若是你运行了上面的例子,你会看到view在两秒钟以后也会更新。惟一的变化是咱们的代码如今被wrapped到了$scope.$apply()中,它会自动触发$rootScope.$digest(),从而让watchers被触发用以更新view。
Note:顺便提一下,你应该使用$timeout service来代替setTimeout(),由于前者会帮你调用$apply(),让你不须要手动地调用它。
并且,注意在以上的代码中你也能够在修改了model以后手动调用没有参数的$apply(),就像下面这样:
$scope.getMessage = function() { setTimeout(function() { $scope.message = ‘Fetched after two seconds'; console.log(‘message:’ + $scope.message); $scope.$apply(); //this triggers a $digest }, 2000); };
以上的代码使用了$apply()的第二种形式,也就是没有参数的形式。须要记住的是你老是应该使用接受一个function做为参数的$apply()方法。这是由于当你传入一个function到$apply()中的时候,这个function会被包装到一个try…catch块中,因此一旦有异常发生,该异常会被$exceptionHandler service处理。
$digest循环会运行多少次?
当一个$digest循环运行时,watchers会被执行来检查scope中的models是否发生了变化。若是发生了变化,那么相应的listener函数就会被执行。这涉及到一个重要的问题。若是listener函数自己会修改一个scope model呢?AngularJS会怎么处理这种状况?
答案是$digest循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有models发生了变化。这就是脏检查(Dirty Checking),它用来处理在listener函数被执行时可能引发的model变化。所以,$digest循环会持续运行直到model再也不发生变化,或者$digest循环的次数达到了10次。所以,尽量地不要在listener函数中修改model。
Note: $digest循环最少也会运行两次,即便在listener函数中并无改变任何model。正如上面讨论的那样,它会多运行一次来确保models没有变化。
结语
我但愿这篇文章解释清楚了$apply和$digest。须要记住的最重要的是AngularJS是否能检测到你对于model的修改。若是它不能检测到,那么你就须要手动地调用$apply()。