Магия AngularJS: никогда не вешайте binding на примитивы

Original author: Aviv Ben-Yosef
  • Translation

Магия AngularJS: никогда не вешайте binding на примитивы


Если вы используете AngularJS, скорее всего вы неоднократно сталкивались с правилом «Не вешайте binding на примитивы». В этом посте я подробно разберу пример, в котором использование примитивов создает проблемы: создание списка элементов , в котором каждый из элементов привязан к строке.

Наш пример

Скажем, вы работаете над приложением с книгами, и у каждой книги есть список тегов. Наивным способом предоставления пользователю возможности редактировать теги будет:
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags">
        <input type="text" ng-model="tag">
    </div>
</div>


(Вы, вероятно, захотите добавить еще одно поле для добавления новых тегов и кнопки для удаления существующих тегов, но мы проигнорируем это для простоты примера.)

Демо нашего примера доступно здесь. Попробуйте отредактировать одно из полей ввода. Казалось бы, все нормально. Но это не так. Если вы приглядитесь повнимательнее, то увидите, что внесенные изменения не синхронизируются с массивом book.tags.

Это происходит потому, что ng-repeat создает child scope для каждого тега и в реальности scopes могут выглядеть так:
bookCtrl scope = { tags: [ 'foo', 'bar', 'baz' ] }
ng-repeat child scopes: { tag: 'foo' }, { tag: 'bar' }, { tag: 'baz' }

В этих child scopes ng-repeat не создает двусторонний binding для значения тега. Это означает, что при изменении первого поля ng-model просто меняет первый child scope на { tag: 'something' }, и это никак не отражается в объекте book.

Теперь вы увидели, как примитивы могут сыграть с вами дурную шутку. Если бы мы для каждого тега вместо строк использовали объекты, то все бы работало, так как тег в child scopes был бы тем же самым instance, что и в book.tags, и изменения его значения (например, tag.name) просто бы работало, даже без 2-way binding.

Но, предположим, что здесь мы не хотим использовать объекты. Как поступить в таком случае?

Неудачная попытка

– Я знаю! – могли бы вы подумать. – Я свяжу ng-repeat напрямую со списком тегов вышестоящего уровня! Давайте попробуем:
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags">
        <input type="text" ng-model="book.tags[$index]">
    </div>
</div>

Таким образом, связав ng-model непосредственно с нужным элементов в списке тегов и не ссылаясь на child-scope, мы заставили наш код работать. Ну, почти. Теперь значения внутри списка будут меняться при вводе текста. Но теперь кое-что еще не так. Можете посмотреть сами. Сделайте это, я подожду.

Как вы можете видеть, когда вы печатаете символ, поле ввода теряет фокус. WTF?

В этом нужно винить ng-repeat. Ради эффективности ng-repeat отслеживает все значения в списке и перерисовывает конкретные элементы, подвергшиеся изменению.

Но примитивы (числа и строки, например) являются immutable в JavaScript. Поэтому, если их нужно изменить, предыдущий instance выбрасывается и используется новый. Таким образом, любое изменения примитива заставляет ng-repeat перерисовать его. В нашем случае это означает, что когда мы избавляемся от старого и добавляем новый, по дороге мы теряем фокус.

Решение

Нам нужно найти способ заставить ng-repeat идентифицировать элементы в списке без зависимости от их примитивного значения. Хорошим вариантом было бы использовать индекс примитива в списке. Но как научить ng-repeat отслеживать элементы в списке?

На наше счастье в Angular 1.2 появился оператор track by:
<div ng-controller="bookCtrl">
    <div ng-repeat="tag in book.tags track by $index">
        <input type="text" ng-model="book.tags[$index]">
    </div>
</div>

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

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

Заметка от переводчика: 29-го мая в Москве состоится митап, посвященный AngularJS, на котором будет обсуждаться в том числе и эта тема.
  • +33
  • 32.4k
  • 9
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    +3
    Отличная статья!
    Ангуляр — классный фреймворк, но начиная на нём писать у новичков в JS (или у людей, которые не задумываются как сделана эта «магия») появляется немало маленьких проблем, которые приходится решать.
      +2
      Спасибо, теперь понятно, для чего нужен track by (попался в примерах, а разъяснения не было).
        +3
        Track by $index здесь нужно использовать первоочерёдно по другой причине — попробуйте указать одинаковые значения в разных элементах списка.

        Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: tag in book.tags, Duplicate key: string:Tyrion
          +1
          Если бы не эта статья, я бы использовал подобный велосипед: jsfiddle.net/m4Pcm/
            0
            У нас тут намечается митап где как раз будем обсуждать эту тему www.meetup.com/WeMakeWeb/events/180554972/
              +1
              место проведения митапа показывается только мемберам, и я не увидел, в каком это городе нашей необъятной
                0
                Москоу сити, уважаемый.
              0
              Действительно полезная статья, спасибо ТС!
                +1
                Track by $index еще нужен, чтобы Angular не добавлял что-то вроде свойства $hash в каждый объект коллекции, поскольку это может иметь влияние на перечисление этих самых свойств.

                Only users with full accounts can post comments. Log in, please.