理解 Angular 中的 $digest() 和 $apply()

翻译自 http://www.sitepoint.com/understanding-angulars-apply-digestjavascript

$digest()$apply()是AngularJS中的两个核心而且有时候容易引人误解的部分。咱们须要深刻理解这二者是如何运做的,从而才能理解AngularJS自己是如何运做的。本文的目的就是为了和你解释,在你的日复一日使用AngularJS编写代码的过程当中,$digest()$apply()是如何确确实实的对你有用的。html

$digest()$apply()的探索

AngularJS为咱们提供了一个广为人知的使人惊叹的功能,那就是数据的双向绑定。这个功能极大的方便了咱们的编码。数据绑定意味着,当视图(view)中的数据发生变化的时候,做用域下的数据模型(model)也会相应的更新。一样的,不管什么时候,当数据模型(model)发生变化的时候,视图(view)也会相应的更新。那么,AngularJS是怎么作到的呢?当你写这样的一个表达式的时候( {{ aModel }} ),AngularJS在背后为这个数据模型(model)设置了一个监听器(watcher),正是因为这个监听器(watcher),不管什么时候当数据模型(model)发生变化的时候,视图(view)也会更新。这个监听器(watcher)与你在AngularJS中设置的其它监听器相似(watcher):java

$scope.$watch('aModel', function ( newValue, oldValue ) {
    // update the DOM with newValue
});

你们都知道,传入$watch()的第二个参数是一个监听函数,而且,不管什么时候当aModel的值发生改变的时候,他都会被调用。对于咱们来讲,这样理解很简单,当aModel发生改变的时候,这个监听函数被调用,而后,更新HTML中的表达式的值。可是,有一个很大的问题在于,AngularJS是怎么知道何时去调用这个监听函数呢?换句话说,AngularJS是怎么知道aModel发生变化的从而去调用相对应的监听函数呢?难道是周期性的运行一个函数来检测数据模型是否发生了变化吗?好吧,这时候,就须要讲到$digest循环了。app

监听器(watcher)是在$digest循环中被启用的。当一个监听器(watcher)被启用的时候,AngularJS就会去评估数据模型(model),而且,当数据模型(model)的值发生改变的时候,就会去调用相应的监听函数。因此,下一个问题就是,$digest循环是怎么开始的。函数

当运行$scope.$digest()的时候,$digest循环就开始了。假设你经过ng-click指令来调用处理函数来改变一个数据模型(model),在这种状况下,AngularJS就会自动调用$digest()触发$digest循环。当$digest循环开始的时候,他就会启动每个监听器(watcher)。这些监听器(watcher)会去检查当前的数据模型(model)中的值是否与最后一次计算的值相同,若是不相同,那么,对应的监听函数就会被执行。结果就是,若是你的视图(view)中有表达式,那么,这些表达式就都会被更新。除了ng-click,还有一些其它的内置指令(或者服务)可让你改变数据模型(好比ng-model$timeout等),而且自动触发$digest循环。this

到目前为止,很棒有没有?可是,仍然有一些小的不足。在上面的例子中,AngularJS并非直接调用$digest(),而是经过$scope.$apply()而后,相应的调用$rootScope.$digest()。这样作的结果就是,一个$digest循环在$rootScope开始,随后,逐步的遍历全部的子做用域来调用监听器(watcher)。编码

如今,假设你给一个按钮添加了ng-click指令,而且为他传递了一个函数。当你点击这个按钮的时候,AngularJS会把这个函数放进$scope.$apply()中进行调用。因此,你的函数可以正常的执行,改变数据模型(若是有的话),而且,$digest循环会开始来保证你的更改会反映到视图(view)中。翻译

注意:$scope.$apply()会自动调用$rootScope.$digest()$apply()函数能够以两种方式运行。第一种是将函数做为参数,而且评估他,而后触发$digest循环。第二种并不传入任何参数,仅仅是执行一个$digest循环。咱们将会知道为何前一种方法是更好更直接的方法。双向绑定

何时须要人为的调用$apply()

若是AngularJS老是将咱们的代码放在$apply()中而且执行$digest循环,那么,何时须要咱们人为的调用$apply呢?事实上,AngularJS已经很明确的告诉咱们了。AngularJS只会关心在AngularJS的执行上下文中的发生的数据模型(model)的变化(好比,改变数据的代码在$apply()里面)。AngularJS内建的指令也会自动触发$digest循环因此任何数据模型(model)的改变都会反映到视图中。可是,若是咱们更改一个不在AngularJS执行上下文中的数据模型(model),就须要人为的调用$apply()来提醒AngularJS数据发生变化了。就像是要告诉AngularJS,咱们改变了一些数据,他应该启用监听器以便于让咱们所作的改变可以反映出来。code

例如,当使用Javascript的setTimeout()函数来更新一个数据模型的时候,AngularJS就没办法知道你改变了数据模型。这种状况下,调用$apply()来触发$digest循环就是你的责任了。相似的,若是你写了一个指令,这个指令是设置了一个DOM事件监听器,更改数据模型的代码在事件处理函数里,那么,也须要调用$apply()来保证更改可以反映出来。

让咱们来看个例子。假设有一个页面,咱们想在页面加载完以后两秒显示一个信息。咱们可能这样写:

<body ng-app="myApp">
    <div ng-controller="MessageController">
        Delayed Message: {{message}}
    </div>  
</body>
/* 没有$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();
});

经过运行这个例子,咱们会发现,在两秒以后,函数开始执行,而且更新数据模型。可是,视图却没有更新。缘由咱们也应该猜到了,咱们忘了调用$apply()。所以,咱们须要像下面这样来写咱们的getMessage()函数。

/* 使用了$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();
});

运行更新后的例子,两秒以后,咱们就可以看到视图更新了。咱们所作的惟一的改变就是将代码放进了$scope.$apply()中。这样,就能自动的去调用$rootScope.$digest(),而后,监听器就会启动,视图就会更新了。

注意:顺便说一句,当须要延时的时候,尽量的使用$timout,这样,就不用人为的去调用$appply()了。

而且,注意看上面的代码,咱们也能够在调用$apply()的时候不传参数。就像下面这样:

$scope.getMessage = function() {
    setTimeout(function() {
        $scope.message = 'Fetched after two seconds';
        console.log('message:' + $scope.message);
        $scope.$apply(); // 这儿就触发了$digest循环
    }, 2000);
};

上面的代码调用$apply()的时候没有传入参数,可是也起做用了。记住,在调用$apply()的时候,应该老是要传入函数参数。由于,当你为$apply()传入函数的时候,这个函数在调用的时候是包含在try..catch中的,而且,任何发生的异常都可以被$exceptionHandler服务所接收。

$digest循环要执行多少次呢

当一个$digest循环运行的时候,监听器就会被执行以查看数据模型是否发生改变。若是改变了,对应的监听函数就会被执行。这就致使了一个很重要的问题,假如一个监听函数本身改变了数据模型,AngularJS怎么知道呢?

答案就是,$digest循环并不仅是运行一次。在当前循环的结束以后,他会再次启动来检查是否有数据发生改变。这被叫作脏检查,用来对那些可能被监听函数所更改的数据模型进行监测。因此,$digest循环会一直保持循环直到再也没有数据模型发生改变,或者,到达最大的循环次数(10次)。保持幂等以及最小化监听函数里的数据模型的更改老是比较好的。

注意:$digest至少会循环两次即便监听函数没有更改任何数据模型。正如上文讨论的那样,他会多运行一次以确保没有数据发生改变。

总结

我但愿这篇文章可以清楚的描述$digest$apply是什么。记住最重要的就是AngularJS是否可以监测到你的更改。若是不能,那么,你就得必须人为调用$apply()

相关文章
相关标签/搜索