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

来源:互联网 发布:windows关闭网络唤醒 编辑:程序博客网 时间:2024/05/18 02:24

原文:http://www.sitepoint.com/understanding-angulars-apply-digest/

$apply()$digest() 是 AngularJS 中的两个核心概念,有时它们令人困惑。要理解 AngularJS 是如何工作的,需要完全理解 $apply()$digest() 是如何工作的。本文的目的是解释 $apply()$digest() 实际上是什么,以及如何在你的日常的 AngularJS 编程中应用它们。

探索 $apply()$digest()

AngularJS提供了一种令人难以置信的功能,叫做双向数据绑定(Two-way Data Binding),它极大地简化了我们的代码编写方式。数据绑定意味着当你在视图(View)中更改某个内容时,scope 模型将会自动更新。类似地,当 scope 模型发生变化时,视图(View)将以新的值进行更新。那么AngularJS是怎么做到的呢?当你编写一个表达式({{aModel}})时,Angular将会在幕后在 scope 模型上设置一个 watcher,当模型发生变化时,它会更新视图。这个 watcher 和你在AngularJS中设置的任何一个 watcher 相同:

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

传递给$watch()的第二个参数称为监听器函数,当aModel的值发生变化时,它就被调用。我们很容易理解,当aModel的值发生改变,这个监听器就会被调用来更新HTML中的表达式。但是,还有一个很重要的问题!Angular是怎么判断什么时候调用这个监听器函数的呢?换句话说,AngularJS是如何知道aModel值是何时发生改变的,从而它可以调用相应的监听器函数呢?它是否定期运行一个函数来检查 scope 模型的值是否已经改变了?好,这就是 $digest 循环的步骤。

$digest 周期中,watcher 会被触发。当一个 watcher 被触发时,AngularJS将评估 scope 模型,如果它发生了变化,则调用相应的监听器函数。那么,我们的下一个问题是,这个 $digest 循环是何时开始的。

调用 $scope.$digest() 后,$digest 循环就开始了。假设你通过ng-click指令在处理程序函数中更改了一个scope模型。在这种情况下,AngularJS会通过调用 $digest() 自动触发一个 $digest 循环。当 $digest 循环开始的时候,它就会触发每一个 watcher。这些 watcher 会检查scope模型的当前值是否与上次计算得到的值不同。如果不同,则执行相应的监听器函数。因此,如果在视图中有任何表达式,它们将被更新。除了ng-click之外,还有其他一些内置的指令/服务可以让你更改模型(例如ng-model、$timeout 等),并自动触发一个 $digest 循环。

到目前为止还不错!但是,这里有一个小问题。在上面的例子中,Angular并不直接调用 $digest()。相反,它调用 $scope.$apply(),而 $scope.$apply() 又会调用 $rootScope.$digest()。因此,一个 $digest 循环开始于 $rootScope,随后会访问所有的child scopes,并在此过程中调用child scopes中的watchers。

现在,假设你将一个ng-click指令附加到一个按钮,并将一个函数名传递给它。当单击按钮时,AngularJS将函数调用包装在 $scope.$apply() 中。因此,你的函数照常执行,更改模型(如果有的话),并开始一个 $digest 循环来确保你的更改反映在视图中。

注意:$scope.$apply() 自动调用 $rootScope.$digest()$apply() 函数有两种形式。第一种接受一个函数作为参数,执行这个函数,并触发一个 $digest 循环。第二种则不需要任何参数,在调用时只触发一个 $digest 循环。接下来,我们将会看到为什么前一个是首选方法。

什么时候需要调用$apply()?

如果AngularJS总是用 $apply() 包装我们的代码并触发一个 $digest 循环,那么什么时候才需要我们手动调用 $apply() 呢?实际上,AngularJS对此有着非常明确的要求,就是它只考虑那些在AngularJS上下文内完成的模型更改(例如,更改模型的代码被包装在 $apply())中。Angular的内置指令就是这样做的,所以你所做的任何模型改变都能反映在视图中。但是,如果你更改了Angular上下文之外的任何模型,那么你需要手动调用 $apply() 来通知更改Angular。这就像是告诉Angular,你正在改变一些模型,它应该触发watchers,这样你的变化才能得到正确的传播。

例如,如果你使用JavaScript的 setTimeout() 函数来更新一个scope模型,那么Angular就无法知道你可能会改变什么。在这种情况下,手动调用 $apply() 就是你的责任,它会触发一个 $digest 循环。类似地,如果你有一个指令,它设置一个DOM事件监听器,并在处理程序函数内部修改了一些模型,那么你需要调用 $apply() 以确保更改生效。

让我们来看一个例子。假设你有一个页面,当页面加载后,你希望在延迟2秒后显示一条消息。你的实现可能类似于下面所示的JavaScript和HTML。

<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();});

通过运行示例,你将在控制台中看到延迟函数在两个秒间隔之后运行,并更新scope模型消息。不过,视图并没有更新。你可能已经猜到了,原因是我们忘记了手动调用 $apply()。因此,我们需要更新 getMessage() 函数,如下所示:

这个 $digest 循环运行了多少次?

当一个 $digest 循环运行时,watcher 会被执行用来查看scope模型是否已经改变。如果有,则调用相应的监听器函数。这就引出了一个重要的问题。如果监听器函数本身更改了scope模型,该怎么办?AngularJS是如何解释这种变化的呢?

答案是,$digest 循环不会只运行一次。在当前循环结束时,它将重新开始检查是否有任何模型发生了变化。这基本上是一个脏检查(Dirty Checking),并且是为了处理任何可能由监听器函数导致的模型更改。因此,$digest 循环将会一直循环,直到不再有模型更改,或者它达到了10这个最大循环数。因此为了保证功能总是好的,请尽量减少在监听器函数内对模型进行更改。

注意:即使监听器函数不改变任何模型,$digest 也会至少运行两次。正如上面所讨论的那样,它会多运行一次,来确保模型是稳定的,并且没有发生变化。

总结

我希望这篇文章解释清楚了什么是 $apply,什么是 $digest。要记住最重要的一点就是,Angular是否能探测到你对于模型的修改。如果不能,那么你必须手动调用 $apply()

原创粉丝点击