题 数据绑定如何在AngularJS中运行?


数据绑定如何在 AngularJS 框架?

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

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

myobject.myproperty="new value";

1803
2018-03-13 10:16


起源


请注意,因为角度为1.0.0rc1,您需要指定ng-model-instant(docs-next.angularjs.org/api/...)让你的现代人彻底更新。否则,它将在模糊事件上更新。 - Sotomajor
Marcello的链接显然已被破坏,所以这里又是: github.com/mhevery/angular.js/blob/master/docs/content/guide/... - riffraff
@orian,那个链接很糟糕。更新到(我假设)是相同的 - docs.angularjs.org/guide/databinding - Kevin Meredith
对于那些仍在阅读这个问题的人,请注意Angular 2.0已经大大改变了自Angular 1.x以来的数据绑定方式,以便使用Web组件并解决下面答案中的许多问题。 - aug


答案:


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

$apply() 方法,这是当你从非AngularJS世界转换到AngularJS世界时调用的函数,调用 $digest()。摘要只是简单的旧脏检查。它适用于所有浏览器,完全可以预测。

对比脏检查(AngularJS)与更改侦听器(KnockoutJS 和 Backbone.js的):虽然脏检查可能看起来很简单,甚至效率低下(我稍后会解决),但事实证明它在语义上一直是正确的,而更改侦听器有很多奇怪的角落情况,并且需要依赖跟踪来制作它在语义上更正确。 KnockoutJS依赖关系跟踪是AngularJS没有的问题的一个聪明特征。

变更听众的问题:

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

性能怎么样?

所以看起来我们很慢,因为脏检查是低效的。这是我们需要查看实数而不仅仅是理论参数的地方,但首先让我们定义一些约束。

人类是:

  •   - 任何超过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。


2660
2018-03-13 23:47



@Mark - 是的,在KO中你只需添加.extend({throttle:500})等待最后一次更改事件后500毫秒,然后再对其进行操作。 - Daniel Earwicker
整个答案很棒,除了“只要它们达到50 fps,任何表现都是浪费,因为人眼无法欣赏它,所以你最好抽出更多东西,而不是让fps更高。”根据您的应用程序,该声明完全不正确。眼睛肯定可以欣赏超过50 fps,并且由于VR显示的各种问题(阅读John Carmack或Michael Abrash的最新版本,特别是后者的GDC 2013 VR讲话),50 fps实际上太慢了。除此之外,你的答案很棒。我只是不想传播错误的信息。 - Nate Bundy
只是想知道,如果您的应用程序像Twitter或评论线程/论坛,并且您实现基于Angular的无限滚动,您可能会遇到“2000条信息”“限制”。单个注释可以轻松地为作者的名称,配置文件img,内容,日期时间等提供多个变量。另外,假设我们有一个巨大的数组用于存储所有注释/帖子,每个脏检查都需要扫描这个数组,我我对吗?这会使浏览器有时会有点滞后,这是一种糟糕的用户体验。在这种情况下,您建议我们做些什么来确保合理的表现? - Lucas
这句话可以很容易地反过来说:“肮脏的检查是一个聪明的特征,对于一个没有淘汰的问题”。 ES6正在使用observables和angular正在摆脱脏检查。现实世界追上了这个答案,并证明这是错误的。 - conical
“任何超过50毫秒的东西都是人类难以察觉的”并非如此。在测试中,我们发现我们的客户可以轻松区分50ms更新延迟(20fps)和16.6ms更新延迟(60fps)。即使人们没有有意识地注册帧率,以前者速度运行的场景总体上也变得更差“感觉如何”。 - Crashworks


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 
数据绑定可能会导致复杂页面出现性能问题。


308
2017-08-22 13:28



是的,我是第二个。我们的应用程序的主要职责是显示不同实体之间的连接。给定页面可能包含10个部分。每个部分都有一张桌子。每个表有2-5个预先过滤器。每个表有2-5列,每列有10行。我们很快就会遇到性能问题,并采用“类似技巧”选项。 - Scott Silvi
可以公平地说,Angular不仅仅是关于数据绑定,而且某些应用程序可能不想使用此功能,正是因为其他人引用的原因?我认为DI和模块化的方法本身是值得的;拥有魔法自动绑定很不错,但在每个现有的实现中都有性能的权衡。对于大多数CRUD网络应用程序而言,Angular的方式可以说是优越的,人们只是试图将其推向极端。有一个支持事件监听的替代方法会很好,但是对于单个框架来说,这可能从根本上说太复杂了? - Jason Boyd
Angular现在有一种方法和绑定一次数据绑定来帮助解决这个问题。此外,它现在具有转发器源的索引,允许您修改列表而无需重建整个内容的dom。 - Mithon
@MW。老实说,我认为bind-once是核心。但它似乎不是。这只是你在编写自己的指令时可以做的事情,基本上是在没有看到它们的情况下链接东西。但是有一个ux mod: github.com/pasvaz/bindonce - Mithon
对于阅读此内容的人来说,未来的呼声:一次绑定现在是Angular v1.3的核心功能,请在此处阅读更多内容: docs.angularjs.org/guide/expression - Nobita


通过脏检查 $scope 目的

Angular保持简单 array 观察者中的 $scope 对象。如果你检查任何 $scope 你会发现它包含一个 array 叫 $$watchers

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

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

如何定义观察者

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

  • 你可以明确地说 $watch 一个 attribute 上 $scope

    $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,你需要它做的任何事情。

遍历每个范围,并根据最后一个值评估和检查每个监视表达式。

如果一个观察者被触发,那么 $scope 是脏的

如果触发了观察者,则应用知道某些内容已发生变化,并且 $scope 被标记为脏。

Watcher函数可以更改其他属性 $scope 或父母 $scope。如果一个 $watcher 功能已被触发,我们不能保证我们的其他 $scopes仍然很干净,所以我们再次执行整个摘要周期。

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

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

我们不断循环 $digest 循环直到消化周期变得干净(全部 $watch 表达式具有与上一个循环中相同的值,或者我们达到摘要限制。默认情况下,此限制设置为10。

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

10 $digest() iterations reached. Aborting!

摘要在机器上很难,但开发人员很容易

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

从机器的角度来看,虽然这种效率非常低,如果我们创造了太多的观察者,它们会减慢我们的应用程序。 Misko引用了大约4000名观众的数字,之后你的应用程序在旧版浏览器上会感觉很慢。

如果你这个限制很容易达到 ng-repeat 超过一大 JSON  array 例如。您可以使用一次性绑定等功能来缓解此问题,以便在不创建观察者的情况下编译模板。

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

每当您的用户与您的应用互动时,您应用中的每位观察者都会至少评估一次。优化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>

142
2018-06-02 12:31



我不同意所说的答案应该在最顶层;知道某事并为特定问题撰写相关/详细答复之间存在差异。有更好的方式获得赞誉。无论如何.. - user2864740
我不怀疑这是真的,但问题问答答案:) - user2864740
很好的答案涵盖了脏检查的行为以及它实际评估的内容,有一点在Misko的答案中并不太清楚。 - bitstrider
精湛而细致的答案。 @superluminary,谢谢你的回答。此外,在阅读了这个答案后,我发现我们不能将非幂等表达式添加为被观察的表达式。 - Mangu Singh Rajpurohit
这应该是最好的答案 - Alexandre Bourlier


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

  1. 通过传递一个函数来观察项目(返回该项目 看着 $watch 方法。
  2. 必须在代码块内对已观看项目进行更改 包裹着 $apply 方法。
  3. 在结束时 $apply 该 $digest 调用哪个方法 通过每个手表和检查,看看他们是否发生了变化 上一次 $digest 跑了。
  4. 如果发现任何更改,则再次调用摘要,直到所有更改稳定为止。

在正常开发中,HTML中的数据绑定语法告诉AngularJS编译器为您创建监视,控制器方法在内部运行 $apply 已经。所以对应用程序开发人员来说,它是透明的。


77
2018-03-13 21:01



何时触发apply方法? - numan salati
@EliseuMonar摘要循环由于某些事件或调用$ apply()而运行,它不会根据计时器定期调用。看到 AngularJS的$ watch功能如何工作? 和 如何在AngularJS中进行绑定和消化工作? - remi
@remi,我并不关心AngularJS的最新版本。他们已经在使用代理或Object.observe吗?如果没有,它们仍处于脏检查时代,它构建一个定时循环以查看模型属性是否已更改。 - Eliseu Monar dos Santos
我读过摘要最多会运行十次 sitepoint.com/understanding-angulars-apply-digest - user137717


我有点想知道这件事。没有setter怎么做 AngularJS 注意到的变化 $scope 目的?它是否会轮询他们?

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

当文档谈到必须打电话时 $apply 手动代码 在外面 AngularJS,它正在讨论代码,这些代码在运行时并非源于 AngularJS 本身在调用堆栈中。


57
2017-09-03 17:45





用图片解释:

数据绑定需要映射

范围中的引用不完全是模板中的引用。当您对两个对象进行数据绑定时,您需要第三个侦听第一个对象并修改另一个对象。

enter image description here

在这里,当你修改 <input>,你碰了 数据REF3。经典的数据绑定机制将会发生变化 数据REF4。那另一个怎么样 {{data}} 表达式会移动吗?

活动导致$ digest()

enter image description here

Angular维持着 oldValue 和 newValue 每一个约束力。每一次之后 角度事件, 有名的 $digest() loop将检查WatchList以查看是否有更改。这些 角度事件 是 ng-clickng-change$http 完成...... $digest() 将循环任何一个 oldValue 不同于 newValue

在上图中,它会注意到data-ref1和data-ref2已经改变。

结论

它有点像鸡蛋和鸡肉。你永远不知道是谁开始,但希望它能按预期大部分时间工作。

另一点是,您可以轻松理解简单绑定对内存和CPU的影响。希望桌面足够肥胖来处理这个问题。手机并不那么强大。


29
2018-05-20 13:33





显然没有定期检查 Scope 附加的对象是否有任何变化。并非所有附加到范围的对象都被观看。范围原型维持一个 $$观察家 。 Scope 只迭代这个 $$watchers 什么时候 $digest 叫做 。

Angular为这些观众添加了观察者

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

$腕表 函数有三个参数:

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

  2. 第二个是侦听器函数,当对象发生更改时将调用该函数。 DOM更改等所有内容都将在此函数中实现。

  3. 第三个是可选参数,它接受一个布尔值。如果它是真的,有角度的深度观察对象&如果它的假角度只是做一个参考观察对象。     $ 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();
       }
};

19
2018-05-22 18:18





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

$表() : -

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

$摘要()  -

此函数遍历$ scope对象中的所有监视,   及其子$ scope对象
     (如果有的话)。当$ digest()迭代时   在手表上,它会检查表达式的值是否有   改变。如果值已更改,AngularJS将使用调用侦听器   新价值和旧价值。 $ digest()函数被调用   只要AngularJS认为有必要。例如,按下一个按钮   单击,或在AJAX调用之后。你可能有一些AngularJS的情况   不会为您调用$ digest()函数。在那种情况下你必须   自己打电话吧。

$适用()  -

Angular会自动神奇地更新那些模型更改   在AngularJS上下文中。当你改变任何模型之外   Angular上下文(如浏览器DOM事件,setTimeout,XHR或第三个   派对图书馆),然后你需要通知Angular的变化   手动调用$ apply()。当$ apply()函数调用完成时   AngularJS在内部调用$ digest(),因此所有数据绑定都是   更新。


12
2018-05-16 15:05



这对我很有帮助 - Jayani Sumudini


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

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

$scope.model.people.name

表格的控制输入:

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

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

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


7
2017-09-18 05:57



我读了5次这个答案,我仍然不明白这里是什么意思。 - sbedulin
答案对我来说似乎很难过 - Aman
@sbedulin LOL:p - Mohamed Sameer


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

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

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


5
2018-06-17 19:28





以下是使用输入字段与AngularJS进行数据绑定的示例。我稍后会解释

HTML代码

<div ng-app="myApp" ng-controller="myCtrl" class="formInput">
     <input type="text" ng-model="watchInput" Placeholder="type something"/>
     <p>{{watchInput}}</p> 
</div>

AngularJS代码

myApp = angular.module ("myApp", []);
myApp.controller("myCtrl", ["$scope", function($scope){
  //Your Controller code goes here
}]);

正如您在上面的示例中所看到的, AngularJS 使用 ng-model 倾听并观察HTML元素上发生的事情,特别是在 input 领域。当事情发生时,做一些事情。在我们的例子中, ng-model 使用小胡子符号绑定到我们的视图 {{}}。输入字段内输入的内容将立即显示在屏幕上。这就是数据绑定的优点,使用最简单的AngularJS。

希望这可以帮助。

请参阅此处的工作示例 Codepen


4
2018-04-06 18:15