数据绑定如何在AngularJS
框架中工作?
我没有在他们的网站上找到技术细节。当数据从视图传播到模型时,它或多或少地清楚它是如何工作的。但是 AngularJS 如何在没有 setter 和 getter 的情况下跟踪模型属性的变化?
我发现有一些JavaScript 观察者可以做这项工作。但Internet Explorer 6和Internet Explorer 7不支持它们。那么 AngularJS 如何知道我改变了例如以下内容并在视图上反映了这一变化?
myobject.myproperty="new value";
AngularJS 会记住该值并将其与之前的值进行比较。这是基本的脏检查。如果值发生变化,则会触发更改事件。
$apply()
方法,当你从非 AngularJS 世界转换到 AngularJS 世界时,你调用$digest()
。摘要只是简单的旧脏检查。它适用于所有浏览器,完全可以预测。
将脏检查(AngularJS)与更改侦听器( KnockoutJS和Backbone.js )进行对比:虽然脏检查看似简单,甚至效率低下(我稍后会解决),但事实证明它在语义上始终是正确的,虽然更改侦听器有很多奇怪的角落情况,并且需要依赖跟踪之类的东西,以使其在语义上更正确。 KnockoutJS 依赖关系跟踪是 AngularJS 没有的问题的一个聪明特征。
所以看起来我们很慢,因为脏检查是低效的。这是我们需要查看实数而不仅仅是理论参数的地方,但首先让我们定义一些约束。
人类是:
慢 - 任何比 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
你会发现它包含一个名为$$watchers
的array
。
每个观察者都是一个包含其他内容的object
attribute
名称,或者更复杂的东西。 $scope
标记为脏。 在 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>