开发者问题收集

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

2012-03-13
359180

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

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

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

myobject.myproperty="new value";
3个回答

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

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

对比脏检查 (AngularJS) 与更改侦听器 ( KnockoutJS Backbone.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 毫秒),任何超过该值的性能都是浪费,因此您最好绘制更多东西,而不是提高 FPS。

Misko Hevery
2012-03-13

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

MW.
2013-08-22

通过脏检查 $scope 对象

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

每个观察者都是一个 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 对象 ,我们遍历其 $$watchers 数组 并评估所有表达式。如果新表达式值与上一个已知值不同,则调用观察程序的函数。此函数可能会重新编译 DOM 的一部分、重新计算 $scope 上的值、触发 AJAX 请求 ,以及您需要它执行的任何操作。

遍历每个范围,并评估每个观察表达式,并与最后一个值进行检查。

如果触发观察程序,则 $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>
superluminary
2015-06-02