数据绑定在 AngularJS 中如何工作?

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

我尚未在其网站上找到技术细节。数据从视图传播到模型时,或多或少地了解了它是如何工作的。但是 AngularJS 如何在没有设置者和获取者的情况下跟踪模型属性的变化?

我发现有些JavaScript 观察程序可以完成这项工作。但是Internet Explorer 6Internet Explorer 7不支持它们。那么 AngularJS 如何知道我更改了以下内容并在视图中反映了此更改?

myobject.myproperty="new value";

答案

AngularJS 会记住该值,并将其与先前的值进行比较。这是基本的脏检查。如果值发生更改,则将触发更改事件。

从非 AngularJS 世界过渡到 AngularJS 世界时调用的$apply()方法调用$digest() 。摘要只是普通的脏检查。它适用于所有浏览器,并且完全可以预测。

对比脏检查(AngularJS)与更改侦听器( KnockoutJSBackbone.js ):尽管脏检查似乎很简单,甚至效率很低(我稍后会解决),但事实证明,它一直在语义上是正确的,变更侦听器有很多奇怪的极端情况,并且需要诸如依赖性跟踪之类的东西以使其在语义上更加正确。 KnockoutJS 依赖项跟踪是 AngularJS 没有的问题的一项聪明功能。

变更侦听器的问题:

  • 该语法非常糟糕,因为浏览器本身不支持它。是的,有代理,但是在所有情况下它们在语义上都不正确,当然,在旧的浏览器上也没有代理。最重要的是,脏检查允许您执行POJO ,而 KnockoutJS 和 Backbone.js 迫使您从其类继承,并通过访问器访问数据。
  • 更改合并。假设您有一组项目。假设您想将项目添加到数组中,就像您要循环添加一样,每次添加时,您都会触发更改事件,即呈现 UI。这对性能非常不利。您想要的是最后只更新一次 UI。更改事件的粒度太细。
  • 更改侦听器会立即在设置器上触发,这是一个问题,因为更改侦听器可以进一步更改数据,从而触发更多的更改事件。这很不好,因为在您的堆栈上,您可能同时发生多个更改事件。假设您有两个数组,无论出于何种原因都需要保持同步。您只能添加一个或另一个,但是每次添加时,都会触发一个更改事件,该事件现在对世界有不一致的看法。这与线程锁定非常相似,因为每个回调都专门执行并完成,因此 JavaScript 避免了线程锁定。更改事件打破了这一点,因为设置员可能会产生意想不到且不明显的深远后果,从而再次造成线程问题。事实证明,您想要做的是延迟侦听器的执行,并保证一次只运行一个侦听器,因此任何代码都可以自由更改数据,并且它知道在执行此操作时不会再运行其他代码。 。

性能如何?

因此,由于脏检查效率低下,因此我们似乎很慢。在这里,我们需要查看实数,而不仅仅是理论上的争论,但是首先让我们定义一些约束。

人类是:

  • - 任何大于 50 毫秒的速度对于人类来说都是无法感知的,因此可以视为 “即时”。

  • 受限 - 您在一个页面上不能真正显示超过 2000 条信息。除此之外,还真是糟糕的 UI,人类无论如何都无法处理。

因此,真正的问题是:您可以在 50 毫秒内在浏览器上进行多少次比较?这是一个很难回答的问题,因为有许多因素在起作用,但这是一个测试用例: http : //jsperf.com/angularjs-digest/6 ,它创建了 10,000 个观察者。在现代浏览器上,这仅需不到 6 毫秒。在Internet Explorer 8 上 ,大约需要 40 毫秒。如您所见,这几天即使在缓慢的浏览器中也不是问题。需要注意的是:比较必须很简单才能适应时间限制... 不幸的是,将慢速比较添加到 AngularJS 中太容易了,因此当您不知道自己要做什么时,很容易构建慢速应用程序是做。但是我们希望通过提供一个检测模块来解决这个问题,该模块将向您显示哪些比较慢。

事实证明,视频游戏和 GPU 使用脏检查方法,特别是因为它是一致的。只要它们超过了监视器的刷新率(通常为 50-60 Hz,或每 16.6-20 ms),那么在那之上的任何性能都是浪费,因此,与提高 FPS 相比,最好是花更多的精力。

Misko 已经很好地描述了数据绑定的工作方式,但是我想对数据绑定的性能问题发表自己的看法。

正如 Misko 所说,大约 2000 个绑定是您开始发现问题的地方,但是无论如何您在页面上都不应拥有超过 2000 条信息。这可能是正确的,但并非每个数据绑定都对用户可见。一旦开始使用双向绑定构建任何类型的窗口小部件或数据网格,就可以轻松实现 2000 个绑定,而不会出现不良的 UX。

例如,考虑一个组合框,您可以在其中键入文本以过滤可用选项。这种控件可能有约 150 个项目,但仍然非常有用。如果它具有某些额外功能(例如,当前选定选项上的特定类),则每个选项将开始获得 3-5 个绑定。将其中三个小部件放在页面上(例如,一个小部件选择一个国家,另一个小部件选择所述国家的城市,第三个小部件选择酒店),您的绑定数已经在 1000 到 2000 之间。

或考虑公司 Web 应用程序中的数据网格。每页 50 行并非不合理,每行可以有 10 到 20 列。如果使用 ng-repeats 构建它,并且 / 或者在某些使用某些绑定的单元中具有信息,则仅使用此网格就可以达到 2000 个绑定。

我发现在使用 AngularJS 时这是一个巨大的问题,到目前为止,我唯一能找到的解决方案是在不使用双向绑定的情况下构造小部件,而不是使用 ngOnce,注销观察者和类似技巧或构造使用 jQuery 和 DOM 操作构建 DOM 的指令。我觉得这违背了首先使用 Angular 的目的。

我很想听听其他解决方法的建议,但是也许我应该写我自己的问题。我想对此发表评论,但事实证明它太长了……

TL; DR
数据绑定可能会导致复杂页面上的性能问题。

通过脏检查$scope对象

角维持简单的array在观察者的$scope的对象。如果检查任何$scope ,将发现它包含一个名为$$watchersarray

每个观察者都是一个包含其他内容的object

  1. 观察者正在监视的表达式。这可能只是一个attribute名称,或更复杂的名称。
  2. 表达式的最后一个已知值。可以对照表达式的当前计算值进行检查。如果值不同,则观察者将触发该功能并将$scope标记为脏。
  3. 如果观察者脏了将执行的功能。

观察者的定义方式

在 AngularJS 中定义监视程序的方式有很多。

  • 您可以在$scope上显式地$watch一个attribute

    $scope.$watch('person.username', validateUnique);
  • 您可以在模板中放置{{}}插值(将在当前$scope上为您创建一个观察器)。

    <p>username: {{person.username}}</p>
  • 您可以要求诸如ng-model类的指令为您定义观察者。

    <input ng-model="person.username" />

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

当我们通过常规渠道(ng-model,ng-repeat 等)与 AngularJS 交互时,指令将触发摘要循环。

摘要循环是$scope及其所有子级深度优先遍历 。对于每个$scope object ,我们遍历其$$watchers array并评估所有表达式。如果新的表达式值与上一个已知值不同,则调用观察者的函数。此函数可能会重新编译 DOM 的一部分,重新计算$scope上的值,触发AJAX request ,您需要执行任何操作。

遍历每个范围,并评估每个 watch 表达式并对照最后一个值进行检查。

如果触发了监视程序,则$scope脏了

如果触发了观察者,则应用程序知道某些更改,并且$scope被标记为脏。

观察者函数可以更改$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 array进行ng-repeat ,则很容易达到此限制。您可以使用一次性绑定之类的功能来缓解这种情况,从而无需创建观察者即可编译模板。

如何避免创建过多的观察者

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

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

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

要么

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

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

当您有很多项目的ng-repeat时,这一点尤其重要。

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

这是我的基本理解。可能是错误的!

  1. 通过将函数(返回要监视的事物)传递给$watch方法来$watch
  2. 对监视项的更改必须在$apply方法包装的代码块内进行。
  3. $apply结束时,将调用$digest方法,该方法将遍历每个手表,并检查它们是否自上次$digest运行以来是否发生了变化。
  4. 如果找到任何更改,则将再次调用摘要,直到所有更改稳定下来。

在正常开发中,HTML 中的数据绑定语法告诉 AngularJS 编译器为您创建监视,并且控制器方法已在$apply内部运行。因此,对于应用程序开发人员而言,这一切都是透明的。

我自己想了一下。没有设置器, AngularJS如何注意到对$scope对象的更改?它会轮询他们吗?

它的实际作用是: AngularJS的胆量已经调用了您修改模型的任何 “正常” 位置,因此它在代码运行后自动为您调用$apply 。假设您的控制器有一个方法,可以通过ng-click某个元素来实现。由于AngularJS为您连接了该方法的调用,因此有机会在适当的位置执行$apply 。同样,对于在视图中正确显示的表达式,这些表达式由AngularJS执行,因此它执行$apply

当文档讨论必须为AngularJS之外的代码手动调用$apply ,它所讨论的代码在运行时并非源于调用堆栈中的AngularJS本身。

图片解释:

数据绑定需要映射

范围中的引用与模板中的引用不完全相同。在对两个对象进行数据绑定时,您需要第三个对象来监听第一个对象并修改另一个对象。

在此处输入图片说明

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

事件导致 $ digest()

在此处输入图片说明

Angular 维护每个绑定的oldValuenewValue 。并且在每个Angular 事件之后 ,著名的$digest()循环将检查 WatchList 以查看是否发生了更改。这些Angular 事件ng-clickng-change$http已完成... 只要任何oldValuenewValue不同, $digest()就会循环。

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

结论

有点像鸡蛋和鸡肉。您永远不会知道谁开始,但希望它在大多数情况下都能按预期工作。

另一点是,您可以轻松理解简单绑定对内存和 CPU 的深层影响。希望台式机足够胖以应付这一问题。手机不是那么强大。

显然,没有定期检查Scope是否附加了对象。并非监视所有附加到作用域的对象。范围原型保持一个$ watchers 。仅当调用$digest时, Scope才通过此$$watchers迭代。

Angular 将监视者添加到 $$ watcher 中以用于每个监视者

  1. {{expression}}- 在您的模板中(以及存在表达式的其他任何地方)或当我们定义 ng-model 时。
  2. $ scope。$ watch('expression / function')—在您的 JavaScript 中,我们可以附加一个范围对象以观看角度。

$ watch函数接受三个参数:

  1. 第一个是 watcher 函数,它仅返回对象,或者我们可以仅添加一个表达式。

  2. 第二个是侦听器函数,当对象发生更改时将调用该函数。诸如 DOM 更改之类的所有事情都将在此功能中实现。

  3. 第三个是一个可选参数,它接受 boolean 值。如果为 true,则 angular deep 监视对象;如果为 false,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 循环是由于调用 $ 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();
       }
};

AngularJS 通过三个强大的函数来处理数据绑定机制: $ watch()$ digest()$ apply() 。多数时候,AngularJS 会调用 $ scope。$ watch()和 $ scope。$ digest(),但在某些情况下,您可能必须手动调用这些函数以使用新值进行更新。

$ watch() :-

该函数用于观察 $ scope 变量的变化。它接受三个参数:表达式,侦听器和相等对象,其中侦听器和相等对象是可选参数。

$ digest() -

此函数遍历 $ scope 对象及其子 $ scope 对象中的所有手表
(如果有)。当 $ digest()遍历表时,它将检查表达式的值是否已更改。如果值已更改,AngularJS 将使用新值和旧值调用侦听器。只要 AngularJS 认为有必要,就会调用 $ digest()函数。例如,单击按钮后或 AJAX 调用后。在某些情况下,AngularJS 可能不会为您调用 $ digest()函数。在这种情况下,您必须自己调用它。

$ apply() -

Angular 只会自动魔术地更新 AngularJS 上下文中的那些模型更改。当您确实在 Angular 上下文之外的任何模型中进行更改(例如浏览器 DOM 事件,setTimeout,XHR 或第三方库)时,则需要通过手动调用 $ apply()来通知 Angular 更改。当 $ apply()函数调用完成时,AngularJS 在内部调用 $ digest(),因此所有数据绑定都将更新。

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

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

$scope.model.people.name

形式的控制输入:

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

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

我通过的模型从服务器数据中更新的示例是,当您要求提供邮政编码时,并且基于书面负载的邮政编码是与该视图关联的殖民地和城市的列表,并且默认情况下为用户设置第一个值。我做得很好,确实发生了什么,就是angularJS有时需要几秒钟来刷新模型,为此,您可以在显示数据时放置微调器。

  1. 单向数据绑定是一种从数据模型中获取值并将其插入 HTML 元素的方法。无法从视图更新模型。它用于经典模板系统中。这些系统仅在一个方向上绑定数据。

  2. Angular 应用程序中的数据绑定是模型和视图组件之间的数据自动同步。

数据绑定使您可以将模型视为应用程序中的单一事实来源。该视图始终是模型的投影。如果模型发生更改,则视图将反映更改,反之亦然。