协慌网

登录 贡献 社区

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

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

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

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

myobject.myproperty="new value";

答案

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

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

将脏检查(AngularJS)与更改侦听器( KnockoutJSBackbone.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。

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对象

Angular 在$scope对象中维护一个简单的观察者array 。如果你检查任何$scope你会发现它包含一个名为$$watchersarray

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

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

如何定义观察者

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

  • 您可以显式$watch $scope上的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 ,以及您需要执行的任何操作。

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

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

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

Watcher 函数可以更改$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>