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

时间:2012-03-13 10:16:37

标签: javascript angularjs data-binding

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

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

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

myobject.myproperty="new value";

14 个答案:

答案 0 :(得分:2713)

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

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

对比脏检查(AngularJS)与更改侦听器(KnockoutJSBackbone.js):虽然脏检查看起来很简单,甚至效率低下(我稍后会解决),但事实证明它在语义上始终是正确的,而变更侦听器有很多奇怪的角落情况,需要依赖跟踪之类的东西才能使它在语义上更正确。对于AngularJS没有的问题,KnockoutJS依赖关系跟踪是一个聪明的功能。

更改侦听器的问题:

  • 语法很糟糕,因为浏览器本身不支持它。是的,有代理,但在所有情况下它们在语义上都不正确,当然在旧浏览器上没有代理。底线是脏检查允许你做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更高。

答案 1 :(得分:315)

Misko已经很好地描述了数据绑定的工作原理,但我想在数据绑定的性能问题上添加我的观点。

正如Misko所说,大约2000个绑定是你开始看到问题的地方,但你不应该在页面上有超过2000条信息。这可能是真的,但并非每个数据绑定对用户都是可见的。一旦你开始用双向绑定构建任何类型的小部件或数据网格,你就可以轻松命中2000个绑定,而不会有糟糕的用户体验。

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

或者考虑企业Web应用程序中的数据网格。每页50行并不合理,每行可以有10-20列。如果使用ng-repeats构建它,和/或在某些使用某些绑定的单元格中有信息,则可能仅使用此网格接近2000个绑定。

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

我很想听听有关处理此问题的其他方法的建议,但也许我应该写自己的问题。我想把它放在评论中,但事实证明这太长了......

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

答案 2 :(得分:151)

通过脏检查$scope对象

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

每个观察者都是$$watchers,其中包含

  1. 观察者正在监视的表达式。这可能只是object名称,或更复杂的内容。
  2. 表达式的最后已知值。可以根据表达式的当前计算值来检查。如果值不同,观察者将触发该功能并将attribute标记为脏。
  3. 如果观察者很脏,将执行的功能。
  4. 如何定义观察者

    在AngularJS中定义观察者的方法有很多种。

    • 您可以在$scope上明确$watch attribute

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

      $scope
    • 您可以要求<p>username: {{person.username}}</p> 等指令为您定义观察者。

      ng-model

    <input ng-model="person.username" /> 周期检查所有观察者的最后一个值

    当我们通过正常渠道(ng-model,ng-repeat等)与AngularJS互动时,指令会触发摘要周期。

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

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

    如果触发了观察者,则request为脏

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

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

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

    如果$rootScope变脏,我们会再次执行整个$digest周期

    我们不断循环$digest周期,直到摘要周期清除(所有$digest表达式与前一周期中的值相同),或者达到摘要限制。默认情况下,此限制设置为10.

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

    $watch

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

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

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

    如果$scope超过大ng-repeat JSON,则很容易达到此限制。您可以使用诸如一次性绑定之类的功能来编写模板而不创建观察者。

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

    每次您的用户与您的应用互动时,您应用中的每位观察者都会至少评估一次。优化AngularJS应用程序的一个重要部分是减少array树中观察者的数量。一种简单的方法是使用一次性绑定

    如果您的数据很少会发生变化,您只能使用:: syntax将其绑定一次,如下所示:

    $scope

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

    只有在渲染包含模板并将数据加载到<p ng-bind="::person.username"></p> 时才会触发绑定。

    如果$scope包含许多项目,这一点尤其重要。

    ng-repeat

答案 3 :(得分:79)

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

  1. 通过传递函数来观察项目(返回要做的事情 观看了$watch方法。
  2. 必须在代码块内对已观看项目进行更改 由$apply方法包裹。
  3. $apply结束时调用了$digest方法 通过每个手表和检查,看看他们是否发生了变化 上次$digest跑了。
  4. 如果发现任何更改,则会再次调用摘要,直到所有更改都稳定下来。
  5. 在正常开发中,HTML中的数据绑定语法告诉AngularJS编译器为您创建监视,控制器方法已在$apply内运行。所以对于应用程序开发人员来说,这一切都是透明的。

答案 4 :(得分:61)

我自己也想了一会儿。如果没有setter,AngularJS如何注意$scope对象的更改?它是否会对它们进行轮询?

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

当文档谈到必须为$apply 之外的代码手动调用AngularJS时,它正在讨论代码,这些代码在运行时不会源自{{ 1}}本身在调用堆栈中。

答案 5 :(得分:32)

用图片解释:

数据绑定需要映射

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

enter image description here

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

事件导致$ digest()

enter image description here

Angular维护每个绑定的oldValuenewValue。在每个 Angular事件之后,着名的$digest()循环将检查WatchList以查看是否有更改。这些 Angular事件ng-clickng-change$http已完成......只要$digest()oldValue不同,newValue就会循环来自public class OrderBLL{ var _iOrderLineDal; public OrderBLL(){ _iOderLineDal = new OrderLineDal(new entityOBject(dbconnectionstring); } public OrderBLL(iOrderLineDal mockOrderLineDal){ _iOrderLineDal = mockOrderLineDal; } }

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

结论

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

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

答案 6 :(得分:21)

显然,没有定期检查Scope附加的对象是否有任何变化。并非所有附加到范围的对象都被观看。范围原型维持 $$观察者Scope仅在调用$$watchers时迭代此$digest

Angular为这些

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

$ watch 功能包含三个参数:

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

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

  4.   
  5. 第三个是可选参数,它接受一个布尔值。如果它是真实的,有角度的深度观看对象&amp;如果它的假角度只是在对象上进行参考。     $ watch的粗略实现看起来像这样

  6.   
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的粗略实现看起来像这样。

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

答案 7 :(得分:14)

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

$ watch(): -

  

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

$ digest() -

  

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

$ apply() -

  

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

答案 8 :(得分:6)

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

例如,如果模型有类似的内容:

$scope.model.people.name

表格的控制输入:

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

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

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

答案 9 :(得分:5)

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

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

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

答案 10 :(得分:4)

以下是使用输入字段与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

答案 11 :(得分:4)

AngularJs支持双向数据绑定 表示您可以访问数据查看 - &gt;控制器&amp; 控制器 - &gt;图

对于前。

<强> 1)

// If $scope have some value in Controller. 
$scope.name = "Peter";

// HTML
<div> {{ name }} </div>

<强> O / P

Peter

您可以在ng-model中绑定数据,例如: -
2)

<input ng-model="name" />

<div> {{ name }} </div>

在上面的示例中,无论用户输入什么,它都会显示在<div>标记中。

如果想将输入从html绑定到控制器: -
3)

<form name="myForm" ng-submit="registration()">
   <label> Name </lbel>
   <input ng-model="name" />
</form>

如果您想在控制器中使用输入name,那么

$scope.name = {};

$scope.registration = function() {
   console.log("You will get the name here ", $scope.name);
};

ng-model绑定我们的视图并在表达式{{ }}中呈现它 ng-model是在视图中向用户显示并与用户交互的数据 因此,在AngularJs中绑定数据很容易。

答案 12 :(得分:3)

Angular.js为我们在视图中创建的每个模型创建一个观察器。无论何时更改模型,都会在模型中附加“ng-dirty”类,因此观察者将观察所有具有“ng-dirty”类别的模型。更新控制器中的值&amp;反之亦然。

答案 13 :(得分:2)

数据绑定:

什么是数据绑定?

每当用户更改视图中的数据时,范围模型中的更改都会发生更新,反之亦然。

怎么可能?

简短答案: 借助摘要循环。

说明: Angular js在范围模型上设置了监视程序,如果模型发生更改,它将触发侦听器功能。

-1

//具有新值的Dom更新代码

});

那么何时以及如何调用观察程序功能?

Watcher函数被称为摘要周期的一部分。

摘要循环被称为自动触发,它是Ang-js内置的指令/服务,例如ng-model,ng-bind,$ timeout,ng-click等。

摘要循环功能:

$scope.$watch('modelVar' , function(newValue,oldValue){

$scope.$digest() -> digest cycle against the current scope. $scope.$apply() -> digest cycle against the parent scope

注意: $ apply()等于$ rootScope。$ digest(),这意味着肮脏的检查从根或顶部或父范围一直到angular js应用程序中的所有子$ scopes开始。

上述功能也可以通过确保您的应用程序是angular js应用程序在上述版本的浏览器IE中工作,这意味着您正在使用script标记中引用的angularjs框架脚本文件。

谢谢。