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


1956

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

我尚未在其网站上找到技术细节。数据从视图传播到模型时,或多或少地了解了它是如何工作的。但是AngularJS如何在没有设置者和获取者的情况下跟踪模型属性的变化?

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

myobject.myproperty="new value";

10
请注意,从angular 1.0.0rc1开始,您需要指定ng-model-instant(docs-next.angularjs.org/api/…)来立即更新现代程序。否则,它将在模糊事件中更新。
2012年

8
马塞罗的链接显然是坏了,所以这里要再次重申:github.com/mhevery/angular.js/blob/master/docs/content/guide/...
痞子

6
@orian,该链接是错误的。更新为(我假设)是相同的-docs.angularjs.org/guide/databinding
Kevin Meredith

11
对于仍在阅读此问题的人,请注意,自Angular 1.x起,Angular 2.0极大地改变了它们进行数据绑定的方式,以便与Web组件一起使用并解决以下答案中的许多问题。
2015年8

Answers:


2744

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

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

对比脏检查(AngularJS)与更改侦听器(KnockoutJSBackbone.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 ms),那么任何性能都是浪费的,因此与提高FPS相比,最好是花更多的精力。


32
@Mark-是的,在KO中,您只需添加.extend({油门:500})即可在上一次更改事件之后等待500毫秒,然后再对其进行操作。
Daniel Earwicker 2013年

158
除了“只要它们能获得50 fps,任何性能都是浪费,因为人眼无法欣赏它,因此,与提高fps相比,您最好抽出更多的东西”,这是一个很好的答案。该声明完全不正确,具体取决于您的应用程序。眼睛绝对可以欣赏到超过50 fps的速度,并且由于显示了VR的各种问题(请阅读John Carmack或Michael Abrash的最新消息,尤其是后者的GDC 2013 VR演讲),因此50 fps实际上太慢了。除此之外,您的答案很棒。我只是不想散布错误信息。
Nate Bundy 2013年

10
@DavidRivers我们是µs,就像utorrent中的1µs = 0.000001s
Thorgeir

33
可以很容易地反驳该声明,因为“脏污检查是剔除没有的问题的巧妙特征”。ES6使用的是可观察物,而angular摆脱了肮脏的检查。现实世界赶上了这个答案,并证明它是错误的。
圆锥形

17
“任何比50毫秒都快的速度是人类无法察觉的”是不正确的。在测试中,我们发现客户可以轻松地区分50ms更新延迟(20fps)和16.6ms更新延迟(60fps)。即使人们没有有意识地记录帧速率,以以前的速度运行的场景的总体“感觉如何”也始终较差。
Crashworks 2014年

323

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


26
是的,我第二次。我们应用的主要职责是显示不同实体之间的连接。给定的页面可能包含10个部分。每个部分都有一个表格。每个表都有2-5个预过滤器。每个表有2-5列,每列10行。我们很快遇到了性能问题,并选择了“类似技巧”。
斯科特·席尔维

10
是否可以说Angular不仅与数据绑定有关,而且某些应用可能出于其他人引用的确切原因可能不希望使用此功能?我认为DI和模块化的方法本身很有价值。具有神奇的自动绑定功能很好,但是在每个现有实现中都需要权衡性能。对于大多数CRUD Web应用程序而言,Angular的方式无疑是优越的,人们只是试图将其发挥到极致而碰壁。支持事件监听的替代方法会很好,但是对于单个框架来说,这根本上太复杂了吗?
杰森·博伊德

8
Angular现在有一种方法和一次绑定数据绑定来解决此问题。此外,它现在具有中继器源的索引,使您可以修改列表,而无需为整个内容重建dom。
GauteLøken2014年

6
@MW。老实说,我认为一次绑定是核心。但似乎并非如此。这只是您编写自己的指令时可以做的事情,基本上可以将内容链接在一起而无需查看它们。但是,它有一个ux mod:github.com/pasvaz/bindonce
GauteLøken

9
任何阅读此书的
Nobita

158

通过脏检查$scope对象

Angular array$scope对象中维护了一个简单的观察者。如果您检查任何内容$scope,都会发现其中包含array被叫$$watchers

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

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

观察者的定义方式

在AngularJS中定义监视程序的方式有很多。

  • 您可以显式$watch显示attributeon $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的一部分,重新​​计算on的值$scope,触发an AJAX request,而您需要执行此操作。

遍历每个范围,并评估每个watch表达式并对照最后一个值进行检查。

如果触发了监视程序,则$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名观察者的数据,然后您的应用在较旧的浏览器上会感觉缓慢。

例如,如果您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>

感谢@ user2864740-尽管Misko的答案应该是最正确的。他知道框架比任何人都好,就是他与堆栈溢出从事这很酷..
superluminary

4
我不同意所说的答案应该放在首位。有所了解与针对特定问题写相关/详细的答复之间是有区别的。有更好的方法获得荣誉。无论如何..
user2864740

1
我毫不怀疑这是真的,但我对问题提出了答案:)
user2864740

3
不错的答案涵盖了脏支票的行为方式及其实际评估,但Misko的答案中并没有太清楚的一件事。

3
精湛而详细的答案。@superluminary,感谢您的回答。而且,在阅读了这个答案之后,我得出的结论是,我们绝不能添加非幂等表达式作为正在观察的表达式。
Mangu Singh Rajpurohit

81

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

  1. 通过将函数(返回要监视的事物)传递给$watch方法来监视项目。
  2. 对观看项目的更改必须在该$apply方法包装的代码块内进行。
  3. 在方法的末尾,将调用该方法,$apply$digest方法将遍历每个手表,并检查自上次$digest运行以来它们是否已更改。
  4. 如果找到任何更改,则将再次调用摘要,直到所有更改稳定下来。

在常规开发中,HTML中的数据绑定语法告诉AngularJS编译器为您创建监视,并且控制器方法已经在内部运行$apply。因此,对于应用程序开发人员而言,这都是透明的。


4
何时触发apply方法?
努曼萨拉蒂

3
@EliseuMonar摘要循环是由于某些事件或调用$ apply()而运行的,它不会根据计时器定期调用。请参阅AngularJS的$ watch函数如何工作?如何结合和AngularJS消化工作?
2014年

1
@remi,我不关心AngularJS的最新版本。他们已经在使用代理或Object.observe吗?如果不是这样,它们仍然处于肮脏的检查时代,该时代建立了一个定时循环以查看模型属性是否已更改。
Eliseu Monar dos Santos,2014年

1
我已经阅读过摘要最多可以运行十次sitepoint.com/understanding-angulars-apply-digest
user137717

62

我自己想了一下。没有二传手,如何AngularJS注意到$scope物体的变化?它会轮询吗?

它的实际作用是:修改过模型的任何“常规”位置都已经从中被调用AngularJS,因此$apply在代码运行之后,它会自动为您调用。假设您的控制器具有某种连接到ng-click某个元素的方法。由于AngularJS为您将该方法的调用连接在一起,因此有机会$apply在适当的位置进行操作。同样,出现在正确的意见表达,这些被执行AngularJS它确实是这样的$apply

当文档讨论必须$apply手动调用之外的AngularJS代码,它是在讨论运行时并非源于AngularJS调用堆栈本身的代码。


32

图片解释:

数据绑定需要映射

范围中的引用与模板中的引用不完全相同。在对两个对象进行数据绑定时,您需要第三个对象来监听第一个对象并修改另一个对象。

在此处输入图片说明

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

事件导致$ digest()

在此处输入图片说明

角保持oldValuenewValue每一个结合。并且在每个Angular事件之后,著名的$digest()循环将检查WatchList以查看是否发生了更改。这些角的事件ng-clickng-change$http完成的... $digest()只要任何将循环oldValue从不同newValue

在上一张图片中,将注意到data-ref1和data-ref2已更改。

结论

有点像鸡蛋和鸡肉。您永远不会知道谁开始,但希望它在大多数情况下都能按预期工作。

另一点是,您可以轻松理解简单绑定对内存和CPU的深层影响。希望台式机足够胖以应付这一问题。手机不是那么强大。


22

显然,没有定期检查Scope附加到它的对象是否有任何变化。并非监视所有附加到作用域的对象。范围原型保持一个$ watchersScope只有$$watchers$digest被调用时才对此进行迭代。

Angular将监视者添加到$$ watcher中以用于每个监视者

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

$ watch函数接受三个参数:

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

  2. 第二个是侦听器函数,当对象发生更改时将调用该函数。诸如DOM更改之类的所有事情都将在此功能中实现。

  3. 第三个是一个可选参数,它接受boolean值。如果为true,则angular deep监视对象;如果为false,Angular则对对象进行参考监视。$ 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循环是由于调用$ 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();
       }
};

15

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

$ watch():-

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

$ digest() -

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

$ apply() -

Angular只会自动魔术地更新AngularJS上下文中的那些模型更改。当您确实在Angular上下文之外的任何模型中进行更改(例如浏览器DOM事件,setTimeout,XHR或第三方库)时,则需要通过手动调用$ apply()来通知Angular更改。当$ apply()函数调用完成时,AngularJS在内部调用$ digest(),因此所有数据绑定都将更新。


7

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

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

$scope.model.people.name

形式的控制输入:

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

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

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


14
我读了5次此答案,但我仍然不明白这是什么意思。
sbedulin

1
对于我来说,答案似乎很困惑
Aman

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

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

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


5

这是一个使用输入字段与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上查看工作示例


5

AngularJs支持双向数据绑定
表示您可以访问数据View-> ControllerController-> View

对于前

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绑定我们的视图并将其呈现为expression {{ }}
ng-model是在视图中向用户显示并与用户进行交互的数据。
因此在AngularJs中绑定数据很容易。


4

Angular.js为我们在视图中创建的每个模型创建一个观察者。每当更改模型时,都会向该模型添加“ ng-dirty”类,因此观察者将观察所有具有“ ng-dirty”类的模型,并在控制器中更新其值,反之亦然。


3

数据绑定:

什么是数据绑定?

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

这怎么可能?

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

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

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

// Dom用新值更新代码

});

那么何时以及如何调用观察程序函数?

监视程序功能被称为摘要循环的一部分。

摘要循环被称为自动触发,它是在ng / model,ng-bind,$ timeout,ng-click等指令/服务中内置的angular js的一部分,可以触发摘要循环。

摘要循环功能:

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

$rootScope.$apply()

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

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

谢谢。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.