Как работает привязка данных в AngularJS? - PullRequest
1893 голосов
/ 13 марта 2012

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

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

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

myobject.myproperty="new value";

Ответы [ 14 ]

2723 голосов
/ 14 марта 2012

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

Метод $apply(), который вы называете, когда переходите из мира, отличного от AngularJS, в мир 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.

316 голосов
/ 22 августа 2013

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

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

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

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

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

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

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

153 голосов
/ 02 июня 2015

Путем грязной проверки объекта $scope

Angular поддерживает простые 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-модель, ng-repeat и т. Д.), Директива запускает цикл дайджеста.

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

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

Если сработает наблюдатель, $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 просто заметит, изменилось ли значение, и сделает остальное приложение совместимым с этим изменением.

С точки зрения машины, хотя это крайне неэффективно и замедлит работу нашего приложения, если мы создадим слишком много наблюдателей. Миско привел цифру в 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>
79 голосов
/ 14 марта 2012

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

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

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

61 голосов
/ 03 сентября 2012

Я задумался над этим сам некоторое время. Без установщиков, как AngularJS уведомляет об изменениях в объекте $scope? Это опрос их?

Что он на самом деле делает, так это: любое «нормальное» место, где вы модифицируете модель, уже вызывалось из кишок AngularJS, поэтому он автоматически вызывает для вас $apply после запуска вашего кода. Скажем, у вашего контроллера есть метод, который подключен к ng-click для некоторого элемента. Поскольку AngularJS связывает вызов этого метода для вас, у него есть шанс сделать $apply в соответствующем месте. Аналогично, для выражений, которые появляются прямо в представлениях, они выполняются AngularJS, поэтому он делает $apply.

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

32 голосов
/ 20 мая 2016

Объясняя с фотографиями:

Для привязки данных требуется отображение

Ссылка в области не является точной ссылкой в ​​шаблоне. Когда вы связываете данные двух объектов, вам нужен третий, который прослушивает первый и модифицирует другой.

enter image description here

Здесь, когда вы изменяете <input>, вы касаетесь data-ref3 . И классический механизм связывания данных изменится data-ref4 . Так как же будут двигаться другие {{data}} выражения?

События приводят к $ digest ()

enter image description here

Angular поддерживает oldValue и newValue каждой привязки. И после каждого Angular события знаменитый цикл $digest() будет проверять список наблюдения, чтобы увидеть, изменилось ли что-то. Эти угловые события ng-click, ng-change, $http завершены ... $digest() будет повторяться до тех пор, пока любой oldValue отличается от newValue.

На предыдущем рисунке он заметит, что data-ref1 и data-ref2 изменились.

Выводы

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

Другой момент заключается в том, что вы можете легко понять влияние простого связывания на память и процессор. Надеюсь, настольные компьютеры достаточно толстые, чтобы справиться с этим. Мобильные телефоны не так сильны.

22 голосов
/ 22 мая 2016

Очевидно, что нет периодической проверки Scope, есть ли какие-либо изменения в объектах, прикрепленных к нему. Наблюдаются не все объекты, прикрепленные к области видимости. Область действия прототипа поддерживает $$ наблюдателей . Scope повторяет это $$watchers только при вызове $digest.

Angular добавляет наблюдателя в $$ watchers для каждого из этих

  1. {{expression}} - в ваших шаблонах (и в любом другом месте, где есть выражение) или когда мы определяем ng-модель.
  2. $ scope. $ Watch (‘expression / function’) - в вашем JavaScript мы можем просто прикрепить объект scope для углового просмотра.

$ watch Функция принимает три параметра:

  1. Первая функция-наблюдатель, которая просто возвращает объект, или мы можем просто добавить выражение.

  2. Вторая функция слушателя, которая будет вызываться при изменении объекта. Все функции, такие как изменения DOM, будут реализованы в этой функции.

  3. Третий является необязательным параметром, который принимает логическое значение. Если это правда, 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 Cycle. Цикл $ digest начинается в результате вызова $ scope. $ Digest (). Предположим, что вы изменили модель $ scope в функции-обработчике с помощью директивы ng-click. В этом случае 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 голосов
/ 16 мая 2016

AngularJS обрабатывает механизм привязки данных с помощью трех мощных функций: $ watch () , $ digest () и $ apply () .В большинстве случаев AngularJS будет вызывать $ scope. $ Watch () и $ scope. $ Digest (), но в некоторых случаях вам, возможно, придется вызывать эти функции вручную для обновления с новыми значениями.

$ watch () : -

Эта функция используется для наблюдения за изменениями переменной в $ scope.Он принимает три параметра: выражение, слушатель и объект равенства, где слушатель и объект равенства являются необязательными параметрами.

$ digest () -

Эта функция перебирает все часы в объекте $ scope и его дочерних объектах $ scope
(если есть).Когда $ digest () выполняет итерацию по наблюдениям, он проверяет, изменилось ли значение выражения.Если значение изменилось, AngularJS вызывает слушателя с новым значением и старым значением.Функция $ digest () вызывается всякий раз, когда AngularJS считает это необходимым.Например, после нажатия кнопки или после вызова AJAX.В некоторых случаях AngularJS не может вызвать функцию $ digest ().В этом случае вы должны назвать это самостоятельно.

$ apply () -

Angular автоматически обновляет только те изменения модели, которыенаходятся в контексте AngularJS.Когда вы изменяете любую модель вне контекста Angular (например, события DOM браузера, setTimeout, XHR или сторонние библиотеки), вам необходимо сообщить Angular об изменениях, вызвав $ apply () вручную.Когда вызов функции $ apply () завершается, AngularJS вызывает $ digest () внутри, поэтому все привязки данных обновляются.

7 голосов
/ 18 сентября 2013

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

Например, если модель имела что-то вроде:

$scope.model.people.name

Контрольный вход формы:

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

Таким образом, если вы измените значение контроллера объекта, оно будет автоматически отражено в представлении.

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

6 голосов
/ 17 июня 2017
  1. Односторонняя привязка данных - это подход, при котором значение берется из модели данных и вставляется в элемент HTML. Нет способа обновить модель из вида. Используется в классических шаблонных системах. Эти системы связывают данные только в одном направлении.

  2. Привязка данных в приложениях Angular - это автоматическая синхронизация данных между компонентами модели и вида.

Привязка данных позволяет вам рассматривать модель как единый источник истины в вашем приложении. Представление является проекцией модели во все времена. Если модель изменилась, представление отражает изменение и наоборот.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...