Spec-Zone.ru › Angular.js 1.6

Улучшить эту документациюКомпилятор HTML

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

Обзор

Компилятор HTML AngularJS позволяет разработчику обучить браузер новому синтаксису HTML. Компилятор позволяет прикрепить поведение к любому HTML-элементу или атрибуту, а также создавать новые HTML-элементы или атрибуты с пользовательским поведением. AngularJS называет эти расширения поведения директивами.

HTML содержит множество конструкций для форматирования HTML статических документов декларативным способом. Например, если что-то нужно выровнять по центру, нет необходимости предоставлять браузеру инструкции о том, как разделить размер окна пополам, чтобы найти центр, и выровнять этот центр с центром текста. Достаточно добавить атрибут align="center" к любому элементу, чтобы добиться желаемого поведения. Такова сила декларативного языка.

Однако декларативный язык также ограничен, так как он не позволяет обучить браузер новому синтаксису. Например, нет простого способа заставить браузер выровнять текст в позиции 1/3 вместо 1/2. Необходимо способ обучить браузер новому синтаксису HTML.

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

Вся эта компиляция происходит в веб-браузере; серверная или предварительная компиляция не задействуются.

Компилятор

Компилятор — это служба AngularJS, которая просматривает DOM, ища атрибуты. Процесс компиляции происходит в два этапа.

  1. Компиляция: обход DOM и сбор всех директив. Результатом является функция связывания.

  2. Связывание: объединение директив со scope и создание живого представления. Любые изменения в модели scope отражаются в представлении, а любые пользовательские взаимодействия с представлением отражаются в модели scope. Это делает модель scope единственным источником истины.

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

Директива

Директива — это поведение, которое должно быть активировано при обнаружении определенных конструкций HTML во время процесса компиляции. Директивы могут быть размещены в именах элементов, атрибутах, именах классов, а также в комментариях. Вот несколько эквивалентных примеров вызова директивы ng-bind.

<span ng-bind="exp"></span>
<span class="ng-bind: exp;"></span>
<ng-bind></ng-bind>
<!-- directive: ng-bind exp -->

Директива — это просто функция, которая выполняется, когда компилятор находит её в DOM. Подробная документация по написанию директив см. в API-директивы.

Вот директива, которая делает любой элемент перетаскиваемым. Обратите внимание на атрибут draggable в элементе <span>.

Наличие атрибута draggable в любом элементе придает элементу новое поведение. Мы расширили словарь браузера естественным для всех, кто знаком с принципами HTML, способом.

Понимание представления

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

Это означает, что любые изменения данных необходимо повторно объединять с шаблоном, а затем innerHTML в DOM. Некоторые проблемы с этим подходом включают:

  1. чтение пользовательского ввода и объединение его с данными
  2. перезапись пользовательского ввода
  3. управление всем процессом обновления
  4. отсутствие выразительности поведения

AngularJS отличается. Компилятор AngularJS обрабатывает DOM, а не строковые шаблоны. Результатом является функция связывания, которая, при объединении с моделью scope, даёт живое представление. Связывание представления и модели scope прозрачно. Разработчик не должен выполнять какие-либо специальные вызовы для обновления представления. И поскольку innerHTML не используется, вы случайно не перепишите пользовательский ввод. Кроме того, директивы AngularJS могут содержать не только привязки текста, но и поведенческие конструкции.

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

Как компилируются директивы

Важно отметить, что AngularJS работает с узлами DOM, а не со строками. Обычно вы не замечаете это ограничение, потому что при загрузке страницы веб-браузер автоматически анализирует HTML и создает DOM.

Компиляция HTML происходит в три этапа:

  1. $compile обходит DOM и сопоставляет директивы.

    Если компилятор обнаруживает, что элемент соответствует директиве, то директива добавляется в список директив, соответствующих элементу DOM. Один элемент может соответствовать нескольким директивам.

  2. После того как все директивы, соответствующие элементу DOM, будут определены, компилятор сортирует директивы по их priority.

    Выполняются функции compile каждой директивы. Каждая функция compile имеет возможность изменить DOM. Каждая функция compile возвращает функцию link . Эти функции комбинируются в «объединённую» функцию связывания, которая вызывает функцию link каждой директивы.

  3. $compile связывает шаблон со scope, вызвав объединённую функцию связывания с предыдущего шага. Это, в свою очередь, вызовет функцию связывания отдельных директив, зарегистрирует слушатели на элементах и настроит $watch с scope, так как каждая директива настроена на это.

Результатом этого является живая привязка между scope и DOM. Таким образом, изменение модели в скомпилированном scope отразится в DOM.

Ниже приведен соответствующий код с использованием службы $compile. Это поможет вам понять, что делает AngularJS внутри.

var $compile = ...; // injected into your code
var scope = ...;
var parent = ...; // DOM element where the compiled template can be appended

var html = '<div ng-bind="exp"></div>';

// Step 1: parse HTML into DOM element
var template = angular.element(html);

// Step 2: compile the template
var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.
var element = linkFn(scope);

// Step 4: Append to DOM (optional)
parent.appendChild(element);

Разница между компиляцией и связыванием

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

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

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

Лучшая практика: Любая операция, которую можно разделить между экземплярами директив, должна быть перенесена в функцию компиляции по соображениям производительности.

Пример «компиляции» против «связывания»

Чтобы понять, давайте рассмотрим реальный пример с ngRepeat:

Hello {{user.name}}, you have these actions:
<ul>
  <li ng-repeat="action in user.actions">
    {{action.description}}
  </li>
</ul>

При компиляции вышеприведенного примера компилятор посещает каждый узел и ищет директивы.

{{user.name}} соответствует директиве интерполяции интерполяции, а ng-repeat соответствует директиве ngRepeat.

Но ngRepeat сталкивается с проблемой.

Ему необходимо уметь клонировать новые элементы <li> для каждого элемента action в user.actions . Это, казалось бы, тривиально, но становится сложнее, когда вы учитываете, что в user.actions могут быть добавлены элементы позже. Это означает, что ему нужно сохранить чистую копию элемента <li> для целей клонирования.

По мере вставки новых action шаблон <li> необходимо клонировать и вставить в ul. Но клонирование элемента <li> недостаточно. Также необходимо скомпилировать <li> , чтобы его директивы, такие как {{action.description}}, оценивались относительно правильного scope.

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

Решение состоит в том, чтобы разделить процесс компиляции на две фазы:

фазу компиляции, где все директивы идентифицируются и сортируются по приоритету, и фазу связывания, где выполняется любая работа, «связывающая» конкретный экземпляр scope и конкретный экземпляр <li>.

Примечание: Связывание означает настройку слушателей в DOM и настройку $watch в Scope для поддержания синхронизации между ними.

ngRepeat работает, предотвращая спуск процесса компиляции в элемент <li> , чтобы он мог создать клон оригинала и самостоятельно обрабатывать вставку и удаление узлов DOM.

Вместо этого директива ngRepeat компилирует <li> отдельно. Результатом компиляции элемента <li> является функция связывания, содержащая все директивы, содержащиеся в элементе <li> , готовые к подключению к определенному клону элемента <li>.

При выполнении ngRepeat отслеживает выражение, и по мере добавления элементов в массив клонирует элемент <li>, создаёт новый scope для клонированного элемента <li> и вызывает функцию связывания для клонированного элемента <li>.

Понимание работы scope с трансклюдированными директивами

Одним из самых распространённых случаев использования директивы является создание переиспользуемых компонентов.

Ниже приведён псевдокод, демонстрирующий работу упрощённого компонента диалога.

<div>
  <button ng-click="show=true">show</button>

  <dialog title="Hello {{username}}."
          visible="show"
          on-cancel="show = false"
          on-ok="show = false; doSomething()">
     Body goes here: {{username}} is {{title}}.
  </dialog>
</div>

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

Вот пример того, как может выглядеть определение шаблона для виджета dialog.

<div ng-show="visible">
  <h3>{{title}}</h3>
  <div class="body" ng-transclude></div>
  <div class="footer">
    <button ng-click="onOk()">Save changes</button>
    <button ng-click="onCancel()">Close</button>
  </div>
</div>

Это не будет правильно отображаться, если не использовать некоторую магию scope.

Первая проблема, которую нужно решить, состоит в том, что шаблон диалогового окна ожидает, что title будет определено. Но мы хотим, чтобы свойство scope шаблона title было результатом интерполяции атрибута <dialog> элемента (то есть "Hello {{username}}"). Кроме того, кнопки ожидают, что функции onOk и onCancel будут присутствовать в scope. Это ограничивает полезность виджета. Для решения проблемы сопоставления мы используем scope для создания локальных переменных, которые шаблон ожидает следующим образом:

scope: {
  title: '@',             // the title uses the data-binding from the parent scope
  onOk: '&',              // create a delegate onOk function
  onCancel: '&',          // create a delegate onCancel function
  visible: '='            // set up visible to accept data-binding
}

Создание локальных свойств в scope виджета создаёт две проблемы:

  1. изоляция — если пользователь забудет установить атрибут title виджета диалога, шаблон диалога будет привязываться к свойству родительского scope. Это непредсказуемо и нежелательно.

  2. трансклюзия — трансклюдированный DOM может видеть локальные переменные виджета, что может перезаписывать свойства, необходимые для привязки данных к трансклюзии. В нашем примере свойство title виджета перекрывает свойство title трансклюзии.

Для решения проблемы отсутствия изоляции директива объявляет новый isolated scope. Изолированный scope не наследует прототипно от родительского scope, и поэтому нам не нужно беспокоиться о случайном перекрытии свойств.

Однако isolated scope создаёт новую проблему: если трансклюдированный DOM является дочерним элементом изолированного scope виджета, то он не сможет связаться ни с чем. По этой причине трансклюдированный scope является дочерним элементом исходного scope, прежде чем виджет создал изолированный scope для своих локальных переменных. Это делает трансклюдированный и изолированный scope виджета братьями.

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

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

transclude: true,
scope: {
    title: '@',             // the title uses the data-binding from the parent scope
    onOk: '&',              // create a delegate onOk function
    onCancel: '&',          // create a delegate onCancel function
    visible: '='            // set up visible to accept data-binding
},
restrict: 'E',
replace: true

Двойная компиляция и как её избежать

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

angular.module('app').directive('addMouseover', function($compile) {
  return {
    link: function(scope, element, attrs) {
      var newEl = angular.element('<span ng-show="showHint"> My Hint</span>');
      element.on('mouseenter mouseleave', function() {
        scope.$apply('showHint = !showHint');
      });

      attrs.$set('addMouseover', null); // To stop infinite compile loop
      element.append(newEl);
      $compile(element)(scope); // Double compilation
    }
  }
})

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

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

Поэтому следует избегать двойной компиляции. В приведённом выше примере нужно скомпилировать только новый элемент:

angular.module('app').directive('addMouseover', function($compile) {
  return {
    link: function(scope, element, attrs) {
      var newEl = angular.element('<span ng-show="showHint"> My Hint</span>');
      element.on('mouseenter mouseleave', function() {
        scope.$apply('showHint = !showHint');
      });

      element.append(newEl);
      $compile(newEl)(scope); // Only compile the new element
    }
  }
})

Другой сценарий — это программирование директивы на уже скомпилированный элемент и повторная компиляция. Смотрите следующий неправильный пример:

<input ng-model="$ctrl.value" add-options>
angular.module('app').directive('addOptions', function($compile) {
  return {
    link: function(scope, element, attrs) {
      attrs.$set('addOptions', null) // To stop infinite compile loop
      attrs.$set('ngModelOptions', '{debounce: 1000}');
      $compile(element)(scope); // Double compilation
    }
  }
});

В этом случае необходимо перехватить первоначальную компиляцию элемента:

  1. Надайте своей директиве свойство terminal и более высокий приоритет, чем директивам, которые не должны компилироваться дважды. В примере компилятор будет компилировать только директивы, у которых приоритет 100 или выше.
  2. Внутри функции compile этой директивы добавьте другие атрибуты директивы в шаблон.
  3. Скомпилируйте элемент, но ограничьте максимальный приоритет, чтобы любые уже скомпилированные директивы (включая директиву addOptions) не компилировались повторно.
  4. В функции link свяжите скомпилированный элемент со scope элемента.
angular.module('app').directive('addOptions', function($compile) {
  return {
    priority: 100, // ngModel has priority 1
    terminal: true,
    compile: function(templateElement, templateAttributes) {
      templateAttributes.$set('ngModelOptions', '{debounce: 1000}');

      // The third argument is the max priority. Only directives with priority < 100 will be compiled,
      // therefore we don't need to remove the attribute
      var compiled = $compile(templateElement, null, 100);

      return function linkFn(scope) {
        compiled(scope) // Link compiled element to scope
      }
    }
  }
});

© 2010–2018 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.6.9/docs/guide/compiler

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API