数据绑定在AngularJS中如何工作?
数据绑定如何在AngularJS
框架中工作?
我没有在他们的网站上find技术细节。 当数据从视图传播到模型时,它如何工作或多或less都很清楚。 但是,AngularJS如何在没有setter和getters的情况下跟踪模型属性的变化?
我发现有JavaScript的观察者可以做这个工作。 但是它们在Internet Explorer 6和Internet Explorer 7中不受支持。 那么AngularJS如何知道我改变了以下内容,并在视图上反映了这种变化?
myobject.myproperty="new value";
AngularJS会记住该值,并将其与之前的值进行比较。 这是基本的脏检查。 如果价值发生变化,那么它会触发变化事件。
$apply()
方法,就是你在从非AngularJS世界转换到AngularJS世界时所调用的$digest()
。 摘要只是简单的旧脏检。 它适用于所有的浏览器,完全可以预测。
为了比较脏检查(AngularJS)和更改监听器( KnockoutJS和Backbone.js ):虽然脏检查看起来可能很简单,甚至是效率低下(我将在后面解决),但事实certificate它始终在语义上是正确的,而更改监听器有很多奇怪的angular落案例,需要像依赖关系跟踪这样的东西,使其在语义上更加正确。 KnockoutJS依赖性跟踪是AngularJS没有的一个聪明的特性。
更改听众的问题:
- 语法很糟糕,因为浏览器本身不支持它。 是的,有代理,但在所有情况下,它们在语义上都是不正确的,当然在旧浏览器上没有代理。 底线是脏检查允许你做POJO ,而KnockoutJS和Backbone.js强迫你从他们的类inheritance,并通过访问器访问你的数据。
- 改变聚结。 假设你有一个项目的数组。 假设你想添加项目到一个数组中,当你循环添加时,每当你添加你正在触发改变的事件,这是呈现UI。 这对性能非常不利。 你想要的只是一次更新UI,最后。 变化事件太细。
- 更改侦听器立即在setter上触发,这是一个问题,因为更改侦听器可以进一步更改数据,从而触发更多的更改事件。 这是不好的,因为在你的堆栈中,你可能会同时发生多个更改事件。 假设你有两个数组需要保持同步,无论出于何种原因。 你只能添加到一个或另一个,但是每次添加时,您都会触发一个更改事件,这个事件现在对世界有不一致的看法。 这与线程locking非常类似,因为每个callback都是独占执行完成的,JavaScript避免了这种情况。 变更事件破坏了这一点,因为变更者可能会产生意想不到的非显而易见的深远后果,从而再次产生线程问题。 事实certificate,你想要做的就是延迟监听器的执行,并且保证一次只能运行一个监听器,因此任何代码都可以自由地改变数据,并且知道在这个过程中没有其他代码在运行。
性能呢?
所以看起来我们很慢,因为脏检是无效的。 这就是我们需要看真实数据的地方,而不是只有理论上的论据,但首先我们来定义一些约束。
人类是:
-
缓慢 – 任何超过50毫秒的速度都是人类无法察觉的,因此可以被视为“即时”。
-
有限 – 你不能在一个页面上向人显示超过2000条信息。 除此之外的任何东西都是非常糟糕的用户界面,人类无法处理这个问题。
所以真正的问题是:在50毫秒内你可以在浏览器上做多less次比较? 这是一个很难回答的问题,因为有很多因素起作用,但这里是一个testing用例: http : //jsperf.com/angularjs-digest/6创build了10,000个观察者。 在现代浏览器上,这需要不到6毫秒。 在Internet Explorer 8上大约需要40毫秒。 正如你所看到的,即使在缓慢的浏览器这些日子,这不是一个问题。 有一个警告:比较需要简单,以适应时间限制…不幸的是,添加到AngularJS的慢比较是太容易了,所以当你不知道你是什么时候,很容易build立缓慢的应用程序是做。 但是我们希望通过提供一个仪器模块来给出答案,这个模块会告诉你哪些是比较慢的。
事实certificate,video游戏和GPU使用脏检查的方法,具体而言是因为它是一致的。 只要他们超过显示器刷新频率(通常是50-60赫兹,或每16.6-20毫秒),任何性能超过这是浪费,所以你最好绘制更多的东西,比得到更高的FPS。
Misko已经很好的描述了数据绑定的工作原理,但是我想在数据绑定的性能问题上增加我的观点。
正如Misko所说,2000年左右的绑定就是你开始看到问题的地方,但是你不应该在一个页面上有2000多条信息。 这可能是事实,但不是每个数据绑定对用户都是可见的。 一旦你开始build立任何forms的双向绑定的窗口小部件或数据网格,你可以轻松地达到2000绑定,没有一个坏ux。
例如,考虑一个combobox,您可以在其中键入文本以过滤可用的选项。 这种控制可能有150个项目,仍然是非常有用的。 如果它有一些额外的function(例如当前select的选项上的一个特定的类),你开始得到每个选项3-5绑定。 将其中三个小部件放在一个页面上(例如一个select一个国家,另一个select所在国家的一个城市,第三个select一个酒店),你已经在1000到2000之间。
或者考虑企业Web应用程序中的数据网格。 每页50行不是不合理的,每行可能有10-20列。 如果你用ng-repeats来构build这个,并且/或者在一些使用绑定的单元格中有信息,那么你可以单独使用这个网格来接近2000个绑定。
我发现在使用AngularJS时这是一个巨大的问题,到目前为止唯一能够find的解决scheme是构build小部件,而不使用双向绑定,而是使用ngOnce,注销观察者和类似的技巧,或构造指令它使用jQuery和DOM操作构buildDOM。 我觉得这首先打破了使用Angular的目的。
我很乐意听取其他处理方法的build议,但是也许我应该写自己的问题。 我想把这个评论,但事实certificate是太长了…
TL; DR
数据绑定可能会导致复杂页面上的性能问题。
通过脏检查$ scope对象
Angular在$ scope对象中维护一个简单的观察者数组。 如果你检查任何$范围,你会发现它包含一个名为$$观察者的数组。
每个观察者都是包含其他事物的对象
- 观察者正在监视的expression。 这可能只是一个属性名称,或者更复杂的东西。
- expression式的最后已知值。 这可以根据expression式的当前计算值进行检查。 如果值不同,观察者将触发该函数并将$ scope标记为脏。
- 如果观察者脏了,将执行一个函数。
如何定义观察者
在AngularJS中定义观察者的方法有很多种。
-
您可以显式$观看$ scope上的一个属性。
$scope.$watch('person.username', validateUnique);
-
您可以在模板中放置一个{{}}插值(在当前$范围内为您创build一个观察器)。
<p>username: {{person.username}}</p>
-
你可以问一个指令,如ng-model来为你定义监视器。
<input ng-model="person.username" />
$ digest循环会检查所有观察者的最后一个值
当我们通过正常通道(ng-model,ng-repeat等)以angular度进行交互时,摘要循环将由指令触发。
摘要循环是对$ scope及其所有子元素的深度优先遍历 。 对于每个$ scope对象,我们遍历其$$观察者数组并评估所有expression式。 如果新的expression式值与最后一个已知值不同,则调用观察者的函数。 这个函数可能会重新编译DOM的一部分,重新计算$ scope的值,触发一个AJAX请求,任何你需要做的事情。
遍历每个范围,并对每个手表expression式进行评估并对照上一个值进行检查。
如果观察者被触发,则$ scope是脏的
如果一个观察者被触发,那么这个应用程序知道某些东西已经改变了,$ scope被标记为脏的。
Watcher函数可以改变$ scope或$ parent作用域上的其他属性。 如果一个$ watcher函数被触发,我们不能保证我们其他的$ scopes仍然是干净的,所以我们再次执行整个摘要循环。
这是因为Angular 1有两种方式绑定,所以数据可以通过$ scope树传回。 我们可能会改变已经被消化的更高价位的价值。 也许我们改变$ rootScope的值。
如果$ digest是脏的,我们再次执行整个$ digest循环
我们不断地遍历$ digest循环,直到摘要循环清理完毕(所有的$ watchexpression式都具有与前一个循环相同的值),或者达到摘要限制。 默认情况下,此限制设置为10。
如果达到摘要限制,Angular将在控制台中引发一个错误:
10 $digest() iterations reached. Aborting!
摘要在机器上很难,但在开发者上很容易
正如您所看到的,每当Angular应用程序中的某些内容发生变化时,Angular将检查$ scope层次结构中的每个观察者以了解如何响应。 对于开发人员来说,这是一个巨大的生产力优势,因为您现在需要编写几乎没有布线代码,Angular只会注意到值是否已更改,并使应用程序的其余部分与更改一致。
从机器的angular度来看,虽然这是非常低效的,并且如果我们创build了太多的观察者,我们的应用就会放慢。 Misko引用了大约4000名观察者的数据,然后你的应用在旧版浏览器上感觉会变慢。
例如,如果你通过一个大的JSON数组重复这个限制,这个限制很容易达到。 您可以使用一次性绑定等function来减轻编译模板而不创build观察者。
如何避免创造太多的观察者
每当用户与您的应用程序进行交互时,应用程序中的每个观察者都将被评估至less一次。 优化Angular应用程序的一大部分是减less$ scope树中观察者的数量。 一个简单的方法是一次性绑定 。
如果你的数据很less改变,你可以使用:: syntax绑定一次,如下所示:
<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>
这是我的基本认识。 这可能是错的!
- 通过传递一个函数(将要观看的事物返回给
$watch
方法)来$watch
。 - 监视项目的更改必须在由
$apply
方法包装的代码块内进行。 - 在
$apply
的末尾,$digest
方法被调用,通过每个手表并检查自上次运行$digest
以来是否发生了变化。 - 如果发现任何更改,则摘要将被再次调用,直到所有更改稳定。
在正常的开发中,HTML中的数据绑定语法告诉AngularJS编译器为你创build手表,并且控制器方法在$apply
已经运行。 所以对于应用程序开发者来说,它是透明的。
我自己想了一会儿。 没有setter, AngularJS
如何通知$scope
对象的变化? 它轮询他们吗?
它实际上做的是这样的:你修改模型的任何“正常”的地方已经从AngularJS的内部调用,所以在你的代码运行后它会自动调用$apply
。 假设你的控制器有一个连接到某个元素的方法。 因为AngularJS将这种方法的调用连接在一起,所以它有机会在适当的地方执行$apply
。 同样,对于在视图中显示正确的expression式,那些由AngularJS执行,所以它$apply
。
当文档中谈到必须手动调用$apply
for AngularJS之外的代码时 ,这里讨论的代码在运行时并不是源自调用堆栈中的AngularJS本身。
用图片解释:
数据绑定需要映射
范围中的引用不完全是模板中的引用。 当你数据绑定两个对象,你需要第三个听第一个和修改另一个。
在这里,当你修改<input>
,你可以触摸data-ref3 。 经典的数据绑定机制将改变data-ref4 。 那么其他{{data}}
expression式将如何移动?
事件导致$ digest()
Angular维护每个绑定的oldValue
和newValue
。 在每个Angular事件之后 ,着名的$digest()
循环将检查WatchList以查看是否有更改。 这些Angular事件是ng-click
, ng-change
, $http
completed …只要任何oldValue
与newValue
不同, $digest()
就会循环。
在前面的图片中,它会注意到data-ref1和data-ref2已经改变。
结论
这有点像鸡蛋和鸡肉。 你永远不知道谁是谁,但希望大多数时候都能按预期工作。
另一点是,你可以很容易地理解一个简单的内存和CPU绑定的影响。 希望台式机足够处理这个问题。 手机不是那么强大。
很显然,没有定期检查Scope
是否有附加对象的任何变化。 并非所有连接到范围的对象都被监视。 范围原型维护$$观察员 。 Scope
只在调用$digest
时通过这个$$watchers
迭代。
Angular向$ $$ watchers中的每一个添加了一个观察者
- {{expression}} – 在您的模板(以及其他任何有expression式的地方)或者我们定义ng模型时。
- $ scope。$ watch('expression / function') – 在您的JavaScript中,我们可以附加angular度的范围对象来观察。
$ watch函数需要三个参数:
首先是一个观察者函数,它只是返回对象,或者我们可以添加一个expression式。
第二个是监听器function,当对象发生变化时会被调用。 所有的东西像DOM的变化将在这个函数中实现。
第三个是一个可选参数,它包含一个布尔值。 如果它的真实,深深的观看对象,如果它的错误的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 Cycle。 $ digest循环开始于调用$ scope。$ digest()。 假定您通过ng-click指令来更改处理函数中的$ scope模型。 在这种情况下,AngularJS通过调用$ digest()自动触发一个$ digest循环。除ng-click以外,还有其他一些内置指令/服务可以让你更改模型(例如ng-model,$ timeout等)并自动触发一个$摘要循环。 $摘要的粗略实现看起来像这样。
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(); } };
我碰巧需要将一个人的数据模型与一个表单连接起来,我所做的只是数据与表单的直接映射。
例如,如果模型有如下的东西:
$scope.model.people.name
表格的控制input:
<input type="text" name="namePeople" model="model.people.name">
这样,如果您修改对象控制器的值,这将在视图中自动反映。
我通过模型的一个例子是从服务器数据更新,当你要求一个邮政编码和邮政编码的基础上写入加载与该视图相关联的殖民地和城市的列表,并默认设置与用户的第一个值。 而且这个我工作得很好,发生了什么,是angularJS
有时需要几秒钟来刷新模型,为此,您可以在显示数据时放置一个微调器。
AngularJS通过三个强大的函数$ watch() , $ digest()和$ apply()来处理数据绑定机制。 大多数情况下,AngularJS会调用$ scope。$ watch()和$ scope。$ digest(),但是在某些情况下,您可能需要手动调用这些函数来更新新的值。
$ watch() : –
这个函数用来观察$ scope中variables的变化。 它接受三个参数:expression式,侦听器和相等对象,其中侦听器和相等对象是可选参数。
$ digest() –
这个函数迭代$ scope对象中的所有监视,以及它的子$ scope对象
(如果有的话)。 当$ digest()遍历手表时,它会检查expression式的值是否已经改变。 如果值已经改变,AngularJS会用新值和旧值来调用监听器。 每当AngularJS认为有必要的时候,都会调用$ digest()函数。 例如,点击button之后,或者在AJAX调用之后。 您可能会遇到一些AngularJS不会为您调用$ digest()函数的情况。 在这种情况下,你必须自己调用它。
$ apply() –
Angular自动神奇地更新AngularJS上下文中的模型更改。 当您在Angular上下文之外的任何模型(如浏览器DOM事件,setTimeout,XHR或第三方库)中进行更改时,则需要手动调用$ apply()来通知Angular。 当$ apply()函数调用完成后,AngularJS在内部调用$ digest(),所有的数据绑定都被更新。
AngularJs支持双向数据绑定 。
意味着你可以访问数据查看 – >控制器和控制器 – >查看
例如
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>
在上面的示例中,无论用户input什么内容,都会在<div>
标签中显示。
如果想要将input从html绑定到控制器:
3)
<form name="myForm" ng-submit="registration()"> <label> Name </lbel> <input ng-model="name" /> </form>
这里如果你想在控制器中使用inputname
,
$scope.name = {}; $scope.registration = function() { console.log("You will get the name here ", $scope.name); };
ng-model
将视图绑定到expression式{{ }}
。
ng-model
是在视图中向用户显示并与用户交互的数据。
所以在AngularJs中绑定数据很容易。
-
单向数据绑定是一种从数据模型中获取值并插入到HTML元素中的方法。 没有办法从视图更新模型。 它在古典模板系统中使用。 这些系统只能在一个方向上绑定数据。
-
Angular应用程序中的数据绑定是模型和视图组件之间数据的自动同步。
数据绑定使您可以将模型视为应用程序中的单一来源。 该视图始终是模型的投影。 如果模型被改变,视图反映了改变,反之亦然。
以下是使用input字段与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
绑定到我们的视图,使用胡子符号{{}}
。 无论input字段内input什么,都会立即显示在屏幕上。 这就是数据绑定的美妙之处,以最简单的forms使用AngularJS。
希望这可以帮助。
在Codepen看到一个可用的例子
Angular.js为我们创build的每个模型创build一个观察器。 每当一个模型被改变时,一个“ng-dirty”类被附加到模型中,所以观察者将观察所有具有“ng-dirty”类的模型,并且在控制器中更新它们的值,反之亦然。