AngularJS框架中的数据绑定是如何工作的?

我还没有在他们的网站上找到技术细节。当数据从视图传播到模型时,它是如何工作的,这或多或少是清楚的。但是AngularJS如何在没有setter和getter的情况下跟踪模型财产的更改?

我发现有JavaScript观察者可能会做这项工作。但Internet Explorer 6和Internet Explorer 7不支持它们。那么AngularJS如何知道我更改了例如以下内容,并将此更改反映在视图上?

myobject.myproperty="new value";

当前回答

我自己也有一段时间对此感到奇怪。没有setters,AngularJS如何注意到$scope对象的更改?它会投票吗?

它实际上是这样做的:您修改模型的任何“正常”位置都已经从AngularJS的内部调用,因此它会在代码运行后自动为您调用$apply。假设你的控制器有一个方法,它连接到某个元素上的ng点击。因为AngularJS为您将该方法的调用连接在一起,所以它有机会在适当的位置执行$apply。同样,对于出现在视图中的表达式,这些表达式由AngularJS执行,因此它执行$apply。

当文档谈到必须为AngularJS之外的代码手动调用$apply时,它指的是运行时不源自调用堆栈中AngularJS本身的代码。

其他回答

通过脏检查$scope对象

Angular在$scope对象中维护一个简单的观察者数组。如果您检查任何$scope,就会发现它包含一个名为$$watcher的数组。

每个观察者都是一个对象,其中包含其他内容

观察程序正在监视的表达式。这可能只是一个属性名称,或者更复杂的东西。表达式的最后一个已知值。这可以根据表达式的当前计算值进行检查。如果值不同,观察者将触发函数并将$scope标记为dirty。如果观察程序是脏的,将执行的函数。

如何定义观察者

在AngularJS中有许多不同的定义观察者的方法。

您可以在$scope上显式地$watch属性。$范围$watch('person.username',validateUnique);您可以在模板中放置{{}}插值(将在当前$scope上为您创建一个观察程序)。<p>用户名:{{person.username}}</p>您可以要求一个指令(如ng模型)为您定义观察者。<input ng model=“person.username”/>

$digest周期检查所有观察者的最后值

当我们通过正常通道(ng模型、ng重复等)与AngularJS交互时,指令将触发摘要循环。

摘要循环是对$scope及其所有子级的深度优先遍历。对于每个$scope对象,我们迭代其$$watcher数组并计算所有表达式。如果新的表达式值与上一个已知值不同,则调用观察者的函数。这个函数可能会重新编译DOM的一部分,重新计算$scope上的值,触发AJAX请求,任何您需要它做的事情。

遍历每个作用域,并根据最后一个值计算和检查每个监视表达式。

如果触发了观察者,则$scope是脏的

如果触发了观察者,则应用程序知道发生了变化,$scope被标记为dirty。

观察者函数可以更改$scope或父$scope上的其他属性。如果触发了一个$watcher函数,我们不能保证我们的其他$scope仍然是干净的,因此我们再次执行整个摘要循环。

这是因为AngularJS具有双向绑定,因此可以将数据传递回$scope树。我们可能会更改已经消化的较高$scope的值。也许我们更改了$rootScope上的值。

如果$digest是脏的,我们将再次执行整个$digest循环

我们不断循环$digest循环,直到消化循环干净(所有$watch表达式的值与上一个循环中的值相同),或者达到消化极限。默认情况下,此限制设置为10。

如果我们达到摘要限制,AngularJS将在控制台中引发错误:

10 $digest() iterations reached. Aborting!

摘要对机器来说很难,但对开发人员来说很容易

正如您所看到的,每当AngularJS应用程序发生变化时,AngularJS都会检查$scope层次结构中的每个观察者,以查看如何响应。对于开发人员来说,这是一个巨大的生产力优势,因为您现在几乎不需要编写任何布线代码,AngularJS只会注意到值是否发生了变化,并使应用程序的其余部分与变化保持一致。

从机器的角度来看,这是非常低效的,如果我们创建了太多的观察者,会减慢我们的应用程序的速度。Misko引用了大约4000名观察者的数据,在你的应用程序在较旧的浏览器上运行缓慢之前。

例如,如果在大型JSON数组上重复,则很容易达到此限制。您可以使用一次性绑定等功能来编译模板,而无需创建观察者。

如何避免创建过多观察者

每次用户与应用程序交互时,应用程序中的每个观察者都将至少进行一次评估。优化AngularJS应用程序的很大一部分是减少$scope树中的观察者数量。一种简单的方法是一次性绑定。

如果您有很少更改的数据,则只能使用::语法将其绑定一次,如下所示:

<p>{{::person.username}}</p>

or

<p ng-bind="::person.username"></p>

只有当呈现包含模板并将数据加载到$scope中时,才会触发绑定。

当你重复很多项目时,这一点尤为重要。

<div ng-repeat="person in people track by username">
  {{::person.username}}
</div>

碰巧我需要将一个人的数据模型与表单链接起来,我所做的是将数据与表单直接映射。

例如,如果模型具有以下内容:

$scope.model.people.name

表单的控制输入:

<input type="text" name="namePeople" model="model.people.name">

这样,如果修改对象控制器的值,这将自动反映在视图中。

我传递的一个从服务器数据更新模型的例子是,当您根据与该视图相关联的殖民地和城市列表的书面加载请求邮政编码和邮编时,默认情况下会向用户设置第一个值。这一点我做得很好,确实发生了,angularJS有时需要几秒钟来刷新模型,为此,您可以在显示数据时放置一个微调器。

Angular.js为我们在视图中创建的每个模型创建一个观察者。每当一个模型被更改时,一个“ng dirty”类就会被附加到该模型上,因此观察者将观察所有具有“ng dirt”类的模型,并在控制器中更新它们的值,反之亦然。

显然,并没有定期检查作用域中附加的对象是否有任何变化。并不是所有附加到作用域的对象都被监视。范围原型通常维护一个$$观察者。当调用$digest时,作用域仅遍历此$$watcher。

Angular为每个$$观察者添加一个观察者

{{表达式}} — 在您的模板中(以及其他任何有表达式的地方),或者在我们定义ng模型时。$范围$watch('表达式/函数') — 在您的JavaScript中,我们只需附加一个范围对象来观察angular。

$watch函数接受三个参数:

第一个是一个观察函数,它只返回对象,或者我们可以添加一个表达式。第二个是侦听器函数,当对象发生更改时将调用该函数。所有诸如DOM更改的事情都将在该函数中实现。第三个是可选参数,它接受布尔值。如果它为真,angular deep会监视对象;如果它为假,angular只会对对象进行引用监视。$watch的大致实现如下

Scope.prototype.$watch = function(watchFn, listenerFn) {
   var watcher = {
       watchFn: watchFn,
       listenerFn: listenerFn || function() { },
       last: initWatchVal  // initWatchVal is typically undefined
   };
   this.$$watchers.push(watcher); // pushing the Watcher Object to Watchers  
};

在Angular中有一个有趣的东西叫做Digest Cycle。$digest循环是调用$scope的结果$digest()。假设您通过ng-click指令更改处理程序函数中的$scope模型。在这种情况下,AngularJS通过调用$digest()自动触发$digest循环。除了ng-click之外,还有几个内置的指令/服务可以让您更改模型(例如ng-model、$timeout等)并自动触发$digest循环。$digest的大致实现如下所示。

Scope.prototype.$digest = function() {
      var dirty;
      do {
          dirty = this.$$digestOnce();
      } while (dirty);
}
Scope.prototype.$$digestOnce = function() {
   var self = this;
   var newValue, oldValue, dirty;
   _.forEach(this.$$watchers, function(watcher) {
          newValue = watcher.watchFn(self);
          oldValue = watcher.last;   // It just remembers the last value for dirty checking
          if (newValue !== oldValue) { //Dirty checking of References 
   // For Deep checking the object , code of Value     
   // based checking of Object should be implemented here
             watcher.last = newValue;
             watcher.listenerFn(newValue,
                  (oldValue === initWatchVal ? newValue : oldValue),
                   self);
          dirty = true;
          }
     });
   return dirty;
 };

如果我们使用JavaScript的setTimeout()函数来更新范围模型,Angular无法知道您可能会更改什么。在这种情况下,我们有责任手动调用$apply(),这将触发$digest循环。类似地,如果您有一个指令设置DOM事件侦听器并更改处理程序函数中的某些模型,则需要调用$apply()以确保更改生效。$apply的主要思想是,我们可以执行一些不知道Angular的代码,这些代码可能仍然会改变作用域上的内容。如果我们将代码包装在$apply中,它将负责调用$digest()。$apply()的粗略实现。

Scope.prototype.$apply = function(expr) {
       try {
         return this.$eval(expr); //Evaluating code in the context of Scope
       } finally {
         this.$digest();
       }
};

用图片解释:

数据绑定需要映射

作用域中的引用与模板中的引用不完全相同。当您数据绑定两个对象时,需要第三个对象侦听第一个对象并修改另一个对象。

在这里,当您修改<input>时,您会触摸data-ref3。经典的数据绑定机制将改变data-ref4。那么其他{{数据}}表达式将如何移动?

事件导致$digest()

Angular维护每个绑定的oldValue和newValue。在每一个Angular事件之后,著名的$digest()循环都会检查WatchList以查看是否发生了变化。这些Angular事件是ng单击、ng更改、$http完成。。。只要任何oldValue与newValue不同,$digest()就会循环。

在上一张图片中,它将注意到data-ref1和data-ref2已更改。

结论

这有点像鸡蛋和鸡肉。你永远不知道是谁开始,但希望大多数时候都能像预期的那样。

另一点是,您可以很容易地理解简单绑定对内存和CPU的影响。希望台式机足够胖,可以处理这个问题。手机没有那么强。