Вопрос: Как работает привязка данных в AngularJS?


Как работает привязка данных в AngularJSфреймворк?

Я не нашел технических подробностей о их сайт , Более или менее ясно, как это работает, когда данные распространяются из представления в модель. Но как AngularJS отслеживает изменения свойств модели без сеттеров и геттеров?

Я обнаружил, что есть Наблюдатели за JavaScript которые могут выполнять эту работу. Но они не поддерживаются в Internet Explorer 6 а также Internet Explorer 7 , Итак, как AngularJS знает, что я изменил, например, следующее и отразил это изменение в представлении?

myobject.myproperty="new value";

1788


источник


Ответы:


AngularJS запоминает значение и сравнивает его с предыдущим значением. Это основная грязная проверка. Если есть изменение в значении, то он запускает событие изменения.

$apply()метод, который вы называете, когда вы переходите из мира без углов в мир AngularJS, звонит $digest(), Дайджест - просто обычная грязная проверка. Он работает во всех браузерах и полностью предсказуем.

Чтобы контрастировать с грязной проверкой (AngularJS) и сменить слушателей ( KnockoutJS а также Backbone.js ): Хотя проверка грязности может показаться простой и даже неэффективной (я расскажу об этом позже), выясняется, что она семантически корректна все время, в то время как у слушателей изменений есть много странных угловых случаев и нужны такие вещи, как отслеживание зависимостей, чтобы сделать это более семантически правильно. Отслеживание зависимостей KnockoutJS - это умная функция для проблемы, которой не существует у AngularJS.

Проблемы с прослушивателями изменений:

  • Синтаксис является жестоким, поскольку браузеры не поддерживают его изначально. Да, есть прокси, но они не являются семантически правильными во всех случаях, и, конечно, в старых браузерах нет прокси-серверов. Суть в том, что грязная проверка позволяет вам делать POJO , тогда как KnockoutJS и Backbone.js вынуждают вас наследовать их классы и получать доступ к вашим данным через аксессуар.
  • Изменение коалесценции. Предположим, у вас есть массив элементов. Предположите, что вы хотите добавлять элементы в массив, поскольку вы добавляете цикл, каждый раз, когда вы добавляете, вы запускаете события с изменениями, которые отображают пользовательский интерфейс. Это очень плохо для производительности. Вы хотите обновить интерфейс только один раз, в конце. События изменений слишком мелкие.
  • Смена слушателей немедленно срабатывает на сеттере, что является проблемой, поскольку прослушиватель изменений может дополнительно изменять данные, что вызывает больше изменений событий. Это плохо, так как в вашем стеке у вас может быть несколько изменений, происходящих сразу. Предположим, у вас есть два массива, которые нужно синхронизировать по любой причине. Вы можете добавлять только одно или другое, но каждый раз, когда вы добавляете, вы запускаете событие изменения, которое теперь имеет непоследовательное представление о мире. Это очень похожая проблема для блокировки потоков, которую JavaScript избегает, поскольку каждый обратный вызов выполняется исключительно и завершается. Изменение событий нарушает это, поскольку сеттеры могут иметь далеко идущие последствия, которые не предназначены и не очевидны, что создает проблему потока снова и снова. Оказывается, что вы хотите отложить выполнение слушателя и гарантировать, что только один прослушиватель работает одновременно, поэтому любой код может свободно изменять данные, и он знает, что никакой другой код не работает, пока он делает это ,

Как насчет производительности?

Поэтому может показаться, что мы медленны, поскольку грязная проверка неэффективна. Здесь нам нужно искать реальные числа, а не просто теоретические аргументы, но сначала давайте определим некоторые ограничения.

Люди:

  • Медленный - Все, что быстрее 50 мс, незаметно для человека и поэтому может считаться «мгновенным».

  • Ограниченное - На одной странице вы не можете показывать более 2000 единиц информации человеку. Что-то большее, чем это действительно плохой пользовательский интерфейс, и люди все равно не могут его обработать.

Итак, настоящий вопрос: сколько сравнений вы можете сделать в браузере за 50 мс? Это трудный вопрос для ответа, поскольку многие факторы вступают в игру, но вот тестовый пример: http://jsperf.com/angularjs-digest/6 который создает 10 000 наблюдателей. В современном браузере это занимает менее 6 мс. На Internet Explorer 8 он занимает около 40 мс. Как вы можете видеть, в наши дни это не проблема даже в медленных браузерах. Существует предостережение: сравнения должны быть простыми в соответствии с временными ограничениями ... К сожалению, слишком легко добавить медленное сравнение в AngularJS, поэтому легко создавать медленные приложения, когда вы не знаете, что вы делаем. Но мы надеемся получить ответ, предоставив инструментальный модуль, который покажет вам, которые являются медленными сравнениями.

Оказывается, видеоигры и графические процессоры используют подход с грязной проверкой, особенно потому, что он согласован. Пока они преодолевают частоту обновления монитора (обычно 50-60 Гц или каждые 16,6-20 мс), любая производительность по сравнению с этим является пустой тратой, поэтому вам лучше рисовать больше, чем получать FPS выше.


2649



Misko уже дал отличное описание того, как работают привязки данных, но я хотел бы добавить свое представление о проблеме производительности с привязкой данных.

Как заявила Мишко, около 2000 привязок - это то, где вы начинаете видеть проблемы, но в любом случае вы не должны иметь более 2000 единиц информации на странице. Это может быть правдой, но не все данные привязываются к пользователю. После того, как вы начнете создавать какой-либо виджет или сетку данных с двусторонней привязкой, вы можете без труда ударил 2000 привязок, не имея плохих ux.

Рассмотрим, например, combobox, где вы можете набирать текст для фильтрации доступных параметров. Такой контроль может иметь ~ 150 элементов и по-прежнему может быть очень полезен. Если у него есть дополнительная функция (например, определенный класс в выбранном в данный момент параметре), вы получаете 3-5 привязок для каждого параметра. Поместите три из этих виджетов на страницу (например, один, чтобы выбрать страну, а другой выбрать город в указанной стране, а третий - выбрать отель), и вы уже где-то между 1000 и 2000 привязками.

Или рассмотрите сетку данных в корпоративном веб-приложении. 50 строк на страницу не являются необоснованными, каждая из которых может содержать 10-20 столбцов. Если вы построите это с помощью ng-повторов и / или получите информацию в некоторых ячейках, которые используют некоторые привязки, вы можете приблизиться к 2000 привязкам только с этой сеткой.

Я считаю, что это огромный проблема при работе с AngularJS, и единственным решением, которое я смог найти до сих пор, является создание виджетов без использования двусторонней привязки, вместо этого использование ngOnce, снятие регистрации с наблюдателей и т. д. или построение директив, которые строят DOM с помощью jQuery и DOM-манипуляция. Я чувствую, что это побеждает цель использования Углового в первую очередь.

Я хотел бы услышать предложения по другим способам справиться с этим, но тогда, возможно, я должен написать свой собственный вопрос. Я хотел поставить это в комментарии, но оказалось, что это слишком долго для этого ...

TL; DR
Связывание данных может вызвать проблемы с производительностью на сложных страницах.


307



Грязная проверка $scopeобъект

Угловая поддерживает простой arrayнаблюдателей в $scopeобъекты. Если вы проверите какие-либо $scopeвы обнаружите, что он содержит arrayназывается $$watchers,

Каждый наблюдатель является 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цикл проверяет всех наблюдателей против их последнего значения

Когда мы взаимодействуем с AngularJS через нормальные каналы (ng-model, ng-repeat и т. Д.), Цикл преобразования будет инициироваться директивой.

Цикл дайджеста - это пересечение глубины $scopeи всех его детей , Для каждого $scope object, мы перебираем его $$watchers arrayи оценивать все выражения. Если новое значение выражения отличается от последнего известного значения, вызывается функция наблюдателя. Эта функция может перекомпилировать часть DOM, пересчитать значение на $scope, вызвать AJAX request, все, что вам нужно для этого.

Каждая область перемещается, и каждое выражение часов оценивается и проверяется на последнее значение.

Если наблюдатель запущен, $scopeгрязный

Если наблюдатель запущен, приложение знает, что что-то изменилось, и $scopeпомечен как грязный.

Функции Watcher могут изменять другие атрибуты на $scopeили на родителя $scope, Если один $watcherфункция была вызвана, мы не можем гарантировать, что наша другая $scopes все еще чисты, и поэтому мы снова выполняем весь цикл дайджестов.

Это связано с тем, что 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>

141



Это мое основное понимание. Это может быть неправильно!

  1. Элементы просматриваются передачей функции (возвращающей вещь, которая должна быть смотрел) на $watchметод.
  2. Изменения наблюдаемых элементов должны быть сделаны в блоке кода завернутый $applyметод.
  3. В конце $apply $digestметод, который идет через каждую из часов и проверок, чтобы увидеть, изменились ли они с тех пор в последний раз $digestпобежал.
  4. Если какие-либо изменения найдены, то дайджест вызывается снова, пока все изменения не стабилизируются.

При нормальной разработке синтаксис привязки данных в HTML говорит компилятору AngularJS о создании часов для вас, а методы контроллера запускаются внутри $applyуже. Поэтому разработчику приложения все прозрачно.


76



I wondered this myself for a while. Without setters how does AngularJS notice changes to the $scope object? Does it poll them?

What it actually does is this: Any "normal" place you modify the model was already called from the guts of AngularJS, so it automatically calls $apply for you after your code runs. Say your controller has a method that's hooked up to ng-click on some element. Because AngularJS wires the calling of that method together for you, it has a chance to do an $apply in the appropriate place. Likewise, for expressions that appear right in the views, those are executed by AngularJS so it does the $apply.

When the documentation talks about having to call $apply manually for code outside of AngularJS, it's talking about code which, when run, doesn't stem from AngularJS itself in the call stack.


57



Explaining with Pictures :

Data-Binding needs a mapping

The reference in the scope is not exactly the reference in the template. When you data-bind two objects, you need a third one that listen to the first and modify the other.

enter image description here

Here, when you modify the <input>, you touch the data-ref3. And the classic data-bind mecanism will change data-ref4. So how the other {{data}} expressions will move ?

Events leads to $digest()

enter image description here

Angular maintains a oldValue and newValue of every binding. And after every Angular event, the famous $digest() loop will check the WatchList to see if something changed. These Angular events are ng-click, ng-change, $http completed ... The $digest() will loop as long as any oldValue differs from the newValue.

In the previous picture, it will notice that data-ref1 and data-ref2 has changed.

Conclusions

It's a little like the Egg and Chicken. You never know who starts, but hopefully it works most of the time as expected.

The other point is that you can understand easily the impact deep of a simple binding on the memory and the CPU. Hopefully Desktops are fat enough to handle this. Mobile phones are not that strong.


29



Obviously there is no periodic checking of Scope whether there is any change in the Objects attached to it. Not all the objects attached to scope are watched . Scope prototypically maintains a $$watchers . Scope only iterates through this $$watchers when $digest is called .

Angular adds a watcher to the $$watchers for each of these

  1. {{expression}} — In your templates (and anywhere else where there’s an expression) or when we define ng-model.
  2. $scope.$watch(‘expression/function’) — In your JavaScript we can just attach a scope object for angular to watch.

$watch function takes in three parameters:

  1. First one is a watcher function which just returns the object or we can just add an expression.

  2. Second one is a listener function which will be called when there is a change in the object. All the things like DOM changes will be implemented in this function.

  3. The third being an optional parameter which takes in a boolean . If its true , angular deep watches the object & if its false Angular just does a reference watching on the object. Rough Implementation of $watch looks like this

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  
};

There is an interesting thing in Angular called Digest Cycle. The $digest cycle starts as a result of a call to $scope.$digest(). Assume that you change a $scope model in a handler function through the ng-click directive. In that case AngularJS automatically triggers a $digest cycle by calling $digest().In addition to ng-click, there are several other built-in directives/services that let you change models (e.g. ng-model, $timeout, etc) and automatically trigger a $digest cycle. The rough implementation of $digest looks like this.

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;
 };

If we use JavaScript’s setTimeout() function to update a scope model, Angular has no way of knowing what you might change. In this case it’s our responsibility to call $apply() manually, which triggers a $digest cycle. Similarly, if you have a directive that sets up a DOM event listener and changes some models inside the handler function, you need to call $apply() to ensure the changes take effect. The big idea of $apply is that we can execute some code that isn't aware of Angular, that code may still change things on the scope. If we wrap that code in $apply , it will take care of calling $digest(). Rough implementation of $apply().

Scope.prototype.$apply = function(expr) {
       try {
         return this.$eval(expr); //Evaluating code in the context of Scope
       } finally {
         this.$digest();
       }
};

19



AngularJS handle data-binding mechanism with the help of three powerful functions : $watch(),$digest()and $apply(). Most of the time AngularJS will call the $scope.$watch() and $scope.$digest(), but in some cases you may have to call these functions manually to update with new values.

$watch() :-

This function is used to observe changes in a variable on the $scope. It accepts three parameters: expression, listener and equality object, where listener and equality object are optional parameters.

$digest() -

This function iterates through all the watches in the $scope object, and its child $scope objects
(if it has any). When $digest() iterates over the watches, it checks if the value of the expression has changed. If the value has changed, AngularJS calls the listener with new value and old value. The $digest() function is called whenever AngularJS thinks it is necessary. For example, after a button click, or after an AJAX call. You may have some cases where AngularJS does not call the $digest() function for you. In that case you have to call it yourself.

$apply() -

Angular do auto-magically updates only those model changes which are inside AngularJS context. When you do change in any model outside of the Angular context (like browser DOM events, setTimeout, XHR or third party libraries), then you need to inform Angular of the changes by calling $apply() manually. When the $apply() function call finishes AngularJS calls $digest() internally, so all data bindings are updated.


12



It happened that I needed to link a data model of a person with a form, what I did was a direct mapping of the data with the form.

For example if the model had something like:

$scope.model.people.name

The control input of the form:

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

That way if you modify the value of the object controller, this will be reflected automatically in the view.

An example where I passed the model is updated from server data is when you ask for a zip code and zip code based on written loads a list of colonies and cities associated with that view, and by default set the first value with the user. And this I worked very well, what does happen, is that angularJS sometimes takes a few seconds to refresh the model, to do this you can put a spinner while displaying the data.


8



  1. The one-way data binding is an approach where a value is taken from the data model and inserted into an HTML element. There is no way to update model from view. It is used in classical template systems. These systems bind data in only one direction.

  2. Data-binding in Angular apps is the automatic synchronisation of data between the model and view components.

Data binding lets you treat the model as the single-source-of-truth in your application. The view is a projection of the model at all times. If the model is changed, the view reflects the change and vice versa.


5



Here is an example of data binding with AngularJS, using an input field. I will explain later

HTML Code

<div ng-app="myApp" ng-controller="myCtrl" class="formInput">
     <input type="text" ng-model="watchInput" Placeholder="type something"/>
     <p>{{watchInput}}</p> 
</div>

AngularJS Code

myApp = angular.module ("myApp", []);
myApp.controller("myCtrl", ["$scope", function($scope){
  //Your Controller code goes here
}]);

As you can see in the example above, AngularJS uses ng-model to listen and watch what happens on HTML elements, especially on input fields. When something happens, do something. In our case, ng-model is bind to our view, using the mustache notation {{}}. Whatever is typed inside the input field is displayed on the screen instantly. And that's the beauty of data binding, using AngularJS in its simplest form.

Hope this helps.

See a working example here on Codepen


4