Статьи

Освоение $ watch в AngularJS

Эта статья была рецензирована Марком Брауном . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

Официальная документация по $watch совсем не исчерпывающая: в конце концов, проблема, которая поразила AngularJS v1 в целом. Даже онлайн-ресурсы, объясняющие, как действовать, в лучшем случае разбросаны.

Таким образом, в конце концов, разработчикам становится трудно выбрать правильный метод для данной ситуации. И это особенно актуально для новичков AngularJS! Результаты могут быть неожиданными или непредсказуемыми, и это неизбежно приводит к ошибкам.

В этой статье я предполагаю некоторое знакомство с концепциями AngularJS. Если вы чувствуете, что нуждаетесь в обновлении, вы можете прочитать о $ scope , binding и $ apply и $ digest .

Проверьте свое понимание

Например, как лучше всего смотреть первый элемент массива? Предположим, у нас есть массив, объявленный в нашей области видимости, $scope.letters = ['A','B','C'];

  • Будет ли $scope.$watch('letters', function () {...}); запустить его обратный вызов, когда мы добавим элемент в массив?
  • Будет ли это, когда мы изменим свой первый элемент?
  • А как насчет $scope.$watch('letters[0]', function () {...}); ? Будет ли это работать так же, или лучше?
  • Выше элементы массива являются примитивными значениями: что если мы заменим первый элемент тем же значением?
  • Теперь предположим, что массив содержит объекты: что происходит?
  • В чем разница между $watch , $watchCollection и $watchGroup ?

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

$ Сфера. $ Часы

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

$ Смотреть

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

  1. Таймауты
  2. UI
  3. Сложные асинхронные вычисления, выполняемые веб-работниками
  4. Аякс звонит

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

Однако для этого вам нужно вызвать $scope.$watch .

Руки вверх

Давайте посмотрим на код для $rootscope.watch() .

Это его подпись: function(watchExp, listener, objectEquality, prettyPrintExpression) .

В деталях его четыре параметра:

  1. watchExp выражение. Это может быть функция или строка, она вычисляется при каждом цикле дайджеста.

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

  2. listener Обратный вызов, запускаемый, когда часы сначала устанавливаются, а затем каждый раз, когда в течение цикла дайджеста обнаруживается изменение значения watchExp . Первоначальный вызов setup предназначен для сохранения начального значения выражения.

  3. objectEquality Если и только если это верно, наблюдатель выполнит глубокое сравнение. В противном случае он выполняет поверхностное сравнение, т.е. сравниваются только ссылки.

    Давайте возьмем массив в качестве примера: $scope.fruit = ["banana", "apple"]; ,

    objectEquality == false означает, что только переназначение поля фрукта вызовет слушателя.

    Нам также необходимо проверить «насколько глубоко» глубокое сравнение: мы рассмотрим это позже.

  4. prettyPrintExpression
    Если он пройден, он переопределяет выражение часов. Этот параметр НЕ предназначен для использования в обычных вызовах $watch() ; он используется внутри синтаксического анализатора выражений .

    Будьте осторожны : как вы сами видите, очень легко столкнуться с неожиданными результатами при передаче 4-го параметра по ошибке.

Теперь мы готовы ответить на некоторые вопросы во введении. Взгляните на наши примеры для этого раздела:

Пожалуйста, не стесняйтесь ознакомиться с ними; Вы можете сравнить разницу в поведении напрямую или следовать порядку в статье.

Смотря массив

Таким образом, вам нужно следить за массивом изменений, но что означает «изменение»?

Предполагая, что ваш контроллер выглядит примерно так:

 app.controller('watchDemoCtrl', ['$scope', function($scope){ $scope.letters = ['A','B','C']; }]); 

один вариант использует вызов как этот:

 $scope.$watch('letters', function (newValue, oldValue, scope) { //Do anything with $scope.letters }); 

В newValue oldValue выше значения newValue и oldValue не oldValue пояснений и будут oldValue каждый раз, когда он вызывается циклом $digest . Значение области scope интуитивно понятно, поскольку содержит ссылку на текущую область.

Но дело в том, когда этот слушатель будет вызван? По сути, вы можете добавлять, удалять, заменять элементы в массиве letters , и ничего не произойдет. Это связано с тем, что по умолчанию $watch предполагает, что вы хотите только ссылочное равенство , поэтому только если вы назначите новое значение в $scope.letters будет $scope.letters обратный вызов.

Если вам нужно воздействовать на изменения какого-либо элемента массива, вам нужно передать true качестве третьего аргумента для наблюдения (т.е. в качестве значения необязательного параметра objectEquality описанного выше).

 $scope.$watch('letters', function (newValue, oldValue, scope) { //Do anything with $scope.letters }, true); 

Смотреть объект

Для объектов сделка не меняется: если objectEquality имеет значение false, вы просто наблюдаете за любым переназначением этой переменной области, в то время как если оно истинно, каждый раз, когда элемент в объекте изменяется, вызывается обратный вызов.

Наблюдая за первым элементом массива

Ничего не стоит, наблюдая массив с objectEquality === true , каждый раз, когда вызывается обратный вызов, newValue и oldValue будут новыми и старыми значениями всего массива. Таким образом, вам придется сравнивать их друг с другом, чтобы понять, что на самом деле изменилось

Скажем, вместо этого вас интересуют изменения первого элемента в массиве (или четвертого — это тот же принцип). Ну, так как Angular удивителен, он позволяет вам просто сделать это: и вы можете выразить это естественным образом в выражении, которое вы передаете в качестве первого аргумента $watch :

 $scope.$watch('letters[4]', function (newValue, oldValue, scope) { //... }, true); 

Что если в массиве всего 2 элемента? Нет проблем, ваш обратный вызов не будет запущен, пока вы не добавите 4-й элемент. Ну, ладно, технически это сработает, когда вы настроите часы, и только тогда, когда вы добавите четвертый элемент.

Если вы oldValue вы увидите, что оба раза он будет undefined , в этом случае. Сравните это с тем, что происходит, если вместо этого просмотрите существующий элемент: в настройке у вас все еще есть oldValue == undefined . Так что ничего, с чем $watch не справится!

Теперь более интересный вопрос: нужно ли передавать здесь objectEquality === true ?

Краткий ответ: извините, короткого ответа нет.

Это действительно зависит:

  • В этом примере, поскольку мы имеем дело с примитивными значениями, нам не нужно глубокое сравнение, поэтому мы можем опустить objectEquality .
  • Но предположим, что у нас есть матрица, скажем, $scope.board = [[1, 2, 3], [4, 5, 6]]; и мы хотим посмотреть первый ряд. Тогда мы, вероятно, хотели бы получить уведомление, когда назначение, подобное $scope.board[0][1] = 7 изменит его.

Наблюдая за полем объекта

Возможно, даже более полезно, чем наблюдение произвольного элемента в массиве, мы можем наблюдать произвольное поле в объекте. Но это не удивительно, правда? В конце концов, массивы в JavaScript — это объекты.

  $scope.obj = {'a': 1, 'b': 2}; $scope.$watch('obj["a"]', function (newValue, oldValue, scope) { // ... }); 

Насколько глубоко это глубокое сравнение?

На этом этапе нам все еще нужно прояснить одну последнюю, но важную деталь: что произойдет, если нам нужно наблюдать за сложным, вложенным объектом, где каждое поле является не примитивным значением? Что-то вроде дерева или графика, или просто некоторые данные JSON.

Давайте проверим это!

Во-первых, нам нужен объект для наблюдения:

  $scope.obj = { 'a': 1, 'b': { 'ba': { 'bab': 2 }, 'bb': [ { 'bb1a': 3, 'bb1b': 4 }, { 'bb2a': 5 } ] } }; 

Давайте установим наши часы для всего объекта: я предполагаю, что к настоящему времени ясно, что для objectEquality должно быть установлено значение true в этом случае.

 $scope.$watch('obj', function (newValue, oldValue, scope) { //... }, true); 

Вопрос в том, будет ли Angular достаточно любезен, чтобы сообщить нам, скажем, о таком присваивании, как $scope.b.bb[1].bb2a = 7; происходит?

И ответ: да, к счастью для нас, это будет (проверьте это на предыдущей демонстрации CodePen).

Другие Методы

$scope.$watchGroup

Является ли $watchGroup() действительно другим методом? Ответ — нет, это не так.

$watchGroup() — это удобный ярлык, который позволяет вам настроить множество наблюдателей с одним и тем же обратным вызовом, передавая массив watchExpressions .

Каждое из переданных выражений будет отслеживаться с использованием стандартного метода $scope.$watch() .

  $scope.$watchGroup(['obj.a', 'obj.b.bb[1]', 'letters[2]'], function(newValues, oldValues, scope) { //... }); 

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

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

Если вы $watchGroup() с демонстрацией ниже для $watchGroup() , вы можете быть удивлены некоторыми тонкостями. Например, unshift вызовет прослушиватель, по крайней мере, до определенного момента: это потому, что при передаче списка выражений в $watchGroup любое из этих срабатываний вызовет выполнение обратного вызова.

Также обратите внимание, что никакие изменения в любом из $scope.obj.b ‘s не приведут к какому-либо обновлению — только присвоение нового значения самому полю b приведет к обновлению.

$scope.$watchCollection

Это еще один удобный способ просмотра массивов или объектов. Для массивов слушатель будет вызван, когда любой из элементов будет заменен, удален или добавлен. Для объектов, когда изменяется любое свойство. Опять же, $watchCollection() не допускает objectEquality , поэтому она будет лишь поверхностно objectEquality элементы / поля и не будет реагировать на изменения их подполей.

Вывод

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

Не стесняйтесь раскошелиться на CodePens и поэкспериментировать с методами в разных контекстах, и не забудьте оставить свой отзыв в области комментариев!

Если вы хотите получить более глубокое понимание некоторых концепций, которые мы рассмотрели в этой статье, вот несколько советов для дальнейшего чтения:

  1. Области применения AngularJS
  2. Понимание Angular’s $ apply () и $ digest ()
  3. Новые шаблоны в обработке событий JavaScript
  4. Наследование прототипа в AngularJS Scopes .
  5. Документация для $watch & co.