Угловые директивы - когда и как использовать компиляцию, контроллер, pre-link и post-link [закрыто]

451

При написании Angular-директивы можно использовать любую из следующих функций для управления поведением DOM, содержимым и внешним видом элемента, для которого объявлена ​​директива:

  • компиляции
  • контроллер
  • предварительно связь
  • пост-ссылка

Кажется, есть некоторая путаница относительно того, какую функцию следует использовать. Этот вопрос охватывает:

Директивные основы

Природа функции, делай и не делай

Смежные вопросы:

Izhaki
источник
27
Что то что?
Haimlit
2
@Ian См .: Перегрузка оператора . По сути это предназначено для вики сообщества. Слишком многие ответы на связанные вопросы являются частичными и не дают полной картины.
Изаки
8
Это отличный контент, но мы просим, ​​чтобы все здесь было в формате вопросов и ответов. Возможно, вы хотели бы разбить это на несколько отдельных вопросов, а затем дать ссылку на них из тега вики?
Флексо
57
Несмотря на то, что этот пост не по теме и в форме блога, он был наиболее полезен для подробного объяснения угловых директив. Пожалуйста, не удаляйте это сообщение, администраторы!
Exegesis
12
Честно говоря, я даже не занимаюсь оригинальными документами. Сообщение о переполнении стека или блог обычно приводят меня в действие в течение нескольких секунд, в отличие от 15-30 минут рвать на себе волосы, пытаясь понять оригинальные документы.
Дэвид

Ответы:

168

В каком порядке выполняются директивные функции?

Для одной директивы

Исходя из следующего плана , рассмотрим следующую HTML-разметку:

<body>
    <div log='some-div'></div>
</body>

Со следующим объявлением директивы:

myApp.directive('log', function() {

    return {
        controller: function( $scope, $element, $attrs, $transclude ) {
            console.log( $attrs.log + ' (controller)' );
        },
        compile: function compile( tElement, tAttributes ) {
            console.log( tAttributes.log + ' (compile)'  );
            return {
                pre: function preLink( scope, element, attributes ) {
                    console.log( attributes.log + ' (pre-link)'  );
                },
                post: function postLink( scope, element, attributes ) {
                    console.log( attributes.log + ' (post-link)'  );
                }
            };
         }
     };  

});

Вывод консоли будет:

some-div (compile)
some-div (controller)
some-div (pre-link)
some-div (post-link)

Мы видим, что compileсначала выполняется, затем controller, затем pre-linkи последний post-link.

Для вложенных директив

Примечание . Следующее не относится к директивам, которые отображают своих потомков в их функции ссылки. Многие Angular-директивы делают это (например, ngIf, ngRepeat или любая другая директива with transclude). Эти директивы будут linkвызывать свои функции до вызова их дочерних директив compile.

Оригинальная HTML-разметка часто состоит из вложенных элементов, каждый из которых имеет свою собственную директиву. Как в следующей разметке (см. Plunk ):

<body>
    <div log='parent'>
        <div log='..first-child'></div>
        <div log='..second-child'></div>
    </div>
</body>

Вывод консоли будет выглядеть так:

// The compile phase
parent (compile)
..first-child (compile)
..second-child (compile)

// The link phase   
parent (controller)
parent (pre-link)
..first-child (controller)
..first-child (pre-link)
..first-child (post-link)
..second-child (controller)
..second-child (pre-link)
..second-child (post-link)
parent (post-link)

Здесь можно выделить две фазы - фазу компиляции и фазу линка .

Фаза компиляции

Когда DOM загружен, Angular начинает фазу компиляции, где он проходит разметку сверху вниз и вызывает compileвсе директивы. Графически мы можем выразить это так:

Изображение, иллюстрирующее цикл компиляции для детей

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

Фаза связи

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

Всякий раз, когда новый экземпляр элемента с директивой отображается в DOM, начинается фаза соединения.

На этом этапе Angular вызывает controller, pre-linkперебирает дочерние элементы и вызывает post-linkвсе директивы, например:

Иллюстрация, демонстрирующая шаги фазы соединения

Izhaki
источник
5
@lzhaki Блок-схема выглядит красиво. Хотите поделиться именем инструмента построения графиков? :)
Мерлин
1
@merlin Я использовал OmniGraffle (но мог бы использовать иллюстратор или inkscape - кроме скорости, нет ничего лучше, чем OmniGraffle, чем другие инструменты построения диаграмм, если рассматривать эту иллюстрацию).
Изаки
2
Плункер @ Anant исчез, так что вот новый: plnkr.co/edit/kZZks8HN0iFIY8ZaKJkA?p=preview Откройте консоль JS, чтобы увидеть операторы журнала
ПОЧЕМУ это не так, когда ng-repeat используется для дочерних директив ??? Смотрите plunk: plnkr.co/edit/HcH4r6GV5jAFC3yOZknc?p=preview
Luckylooke
@Luckylooke У вашего планка нет дочерних элементов с директивой под ng-repeat (то есть повторяется шаблон с директивой. Если бы это было так, вы бы увидели, что их компиляция вызывается только после ссылки ng-repeat.
Изаки
90

Что еще происходит между этими вызовами функций?

Различные функции директив выполнены из двух других в пределах угловых функций , вызываемых $compile(где директива compileвыполняются) и внутренняя функцией , называемой nodeLinkFn(где директивы controller, preLinkи postLinkисполняются). В угловой функции происходят разные вещи до и после вызова директивных функций. Возможно, наиболее заметно это рекурсия ребенка. На следующем упрощенном рисунке показаны основные этапы на этапах компиляции и компоновки:

Иллюстрация, показывающая фазы углового компиляции и компоновки

Чтобы продемонстрировать эти шаги, давайте используем следующую разметку HTML:

<div ng-repeat="i in [0,1,2]">
    <my-element>
        <div>Inner content</div>
    </my-element>
</div>

Со следующей директивой:

myApp.directive( 'myElement', function() {
    return {
        restrict:   'EA',
        transclude: true,
        template:   '<div>{{label}}<div ng-transclude></div></div>'
    }
});

Compile

В compileAPI выглядит следующим образом:

compile: function compile( tElement, tAttributes ) { ... }

Часто параметры имеют префикс t для обозначения элементов и предоставленных атрибутов, которые относятся к исходному шаблону, а не к экземпляру.

Перед вызовом compileвключенный контент (если есть) удаляется, а шаблон применяется к разметке. Таким образом, элемент, предоставленный compileфункции, будет выглядеть так:

<my-element>
    <div>
        "{{label}}"
        <div ng-transclude></div>
    </div>
</my-element>

Обратите внимание, что включенный контент не вставляется повторно в этот момент.

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

Создание экземпляра

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

контроллер

controllerAPI включает в себя:

controller: function( $scope, $element, $attrs, $transclude ) { ... }

При входе в фазу связи функция связи, возвращаемая через $compile, теперь снабжена областью действия.

Во-первых, функция ссылки создает дочернюю область ( scope: true) или изолированную область ( scope: {...}) по запросу.

Затем выполняется контроллер, снабженный областью действия элемента экземпляра.

Pre-ссылка

В pre-linkAPI выглядит следующим образом:

function preLink( scope, element, attributes, controller ) { ... }

Практически ничего не происходит между вызовом директивы .controllerи .preLinkфункции. Angular по-прежнему дают рекомендации относительно того, как каждый из них должен использоваться.

После .preLinkвызова функция link будет проходить через каждый дочерний элемент - вызывая правильную функцию link и присоединяя к ней текущую область (которая служит родительской областью для дочерних элементов).

Пост-ссылка

post-linkAPI аналогичен из pre-linkфункции:

function postLink( scope, element, attributes, controller ) { ... }

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

Это значит, что к тому времени, когда .postLinkзвонят, дети «вживую» готовы. Это включает в себя:

  • привязка данных
  • применяется включение
  • объем прилагается

Шаблон на этом этапе будет выглядеть так:

<my-element>
    <div class="ng-binding">
        "{{label}}"
        <div ng-transclude>                
            <div class="ng-scope">Inner content</div>
        </div>
    </div>
</my-element>
Izhaki
источник
3
Как вы создали этот рисунок?
Рой Намир
6
@RoyiNamir Omnigraffle.
Изаки
43

Как объявить различные функции?

Компиляция, Контроллер, Пре-ссылка и Пост-ссылка

Если нужно использовать все четыре функции, директива будет выглядеть следующим образом:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.
            return {
                pre: function preLink( scope, element, attributes, controller, transcludeFn ) {
                    // Pre-link code goes here
                },
                post: function postLink( scope, element, attributes, controller, transcludeFn ) {
                    // Post-link code goes here
                }
            };
        }
    };  
});

Обратите внимание, что compile возвращает объект, содержащий функции pre-link и post-link; в угловом жаргоне мы говорим, что функция компиляции возвращает функцию шаблона .

Компиляция, контроллер и пост-ссылка

Если в pre-linkэтом нет необходимости, функция компиляции может просто вернуть функцию post-link вместо объекта определения, например, так:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.
            return function postLink( scope, element, attributes, controller, transcludeFn ) {
                    // Post-link code goes here                 
            };
        }
    };  
});

Иногда желательно добавить compileметод после определения (post) linkметода. Для этого можно использовать:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        compile: function compile( tElement, tAttributes, transcludeFn ) {
            // Compile code goes here.

            return this.link;
        },
        link: function( scope, element, attributes, controller, transcludeFn ) {
            // Post-link code goes here
        }

    };  
});

Контроллер и пост-ссылка

Если функция компиляции не требуется, можно полностью пропустить ее объявление и предоставить функцию post-link под linkсвойством объекта конфигурации директивы:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        controller: function( $scope, $element, $attrs, $transclude ) {
            // Controller code goes here.
        },
        link: function postLink( scope, element, attributes, controller, transcludeFn ) {
                // Post-link code goes here                 
        },          
    };  
});

Нет контроллера

В любом из приведенных выше примеров можно просто удалить controllerфункцию, если она не нужна. Так, например, если post-linkнужна только функция, можно использовать:

myApp.directive( 'myDirective', function () {
    return {
        restrict: 'EA',
        link: function postLink( scope, element, attributes, controller, transcludeFn ) {
                // Post-link code goes here                 
        },          
    };  
});
Izhaki
источник
31

В чем разница между исходным шаблоном и шаблоном экземпляра ?

Тот факт, что Angular допускает манипулирование DOM, означает, что входная разметка в процессе компиляции иногда отличается от выходной. В частности, некоторая входная разметка может быть клонирована несколько раз (например, с помощью ng-repeat) перед передачей в DOM.

Угловая терминология немного противоречива, но все же различает два типа разметки:

  • Исходный шаблон - разметка для клонирования, если это необходимо. При клонировании эта разметка не будет отображаться в DOM.
  • Шаблон экземпляра - фактическая разметка, которая будет отображаться в DOM. Если клонирование вовлечено, каждый экземпляр будет клоном.

Следующая разметка демонстрирует это:

<div ng-repeat="i in [0,1,2]">
    <my-directive>{{i}}</my-directive>
</div>

HTML-код источника определяет

    <my-directive>{{i}}</my-directive>

который служит исходным шаблоном.

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

Izhaki
источник
23

Функция компиляции

compileФункция каждой директивы вызывается только один раз, когда Angular загружается.

Официально это место для выполнения (исходных) шаблонных манипуляций, которые не связаны с областью действия или привязкой данных.

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

<tr ng-repeat="raw in raws">
    <my-raw></my-raw>
</tr>

<my-raw>Директива будет оказывать определенный набор DOM разметки. Таким образом, мы можем либо:

  • Разрешить ng-repeatдублирование исходного шаблона ( <my-raw>), а затем изменить разметку каждого шаблона экземпляра (вне compileфункции).
  • Измените исходный шаблон, чтобы задействовать нужную разметку (в compileфункции), а затем разрешите ng-repeatдублировать его.

Если в rawsколлекции 1000 предметов , последний вариант может быть быстрее предыдущего.

Делать:

  • Управляйте разметкой, чтобы она служила шаблоном для экземпляров (клонов).

Не делайте

  • Присоедините обработчики событий.
  • Осмотрите дочерние элементы.
  • Настройте наблюдения на атрибуты.
  • Установите часы на прицеле.
Izhaki
источник
20

Функция контроллера

controllerФункция каждой директивы вызывается всякий раз, когда создается новый связанный элемент.

Официально, controllerфункция где один:

  • Определяет логику контроллера (методы), который может быть разделен между контроллерами.
  • Инициирует переменные области видимости.

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

Делать:

  • Определить логику контроллера
  • Инициировать переменные области

Не делайте:

  • Проверьте дочерние элементы (они еще не могут быть отображены, привязаны к области и т. Д.).
Izhaki
источник
Рад, что вы упомянули Controller в директиве, это отличное место для инициализации области. Мне было трудно обнаружить это.
jsbisht
1
Контроллер НЕ «Инициирует область действия», он получает доступ только к области, уже инициированной независимо от него.
Дмитрий Зайцев
@DmitriZaitsev хорошее внимание к деталям. Я исправил текст.
Изаки
19

Функция пост-ссылки

Когда post-linkвызывается функция, выполняются все предыдущие шаги - привязка, включение и т. Д.

Обычно это место для дальнейших манипуляций с отображаемым DOM.

Делать:

  • Манипулировать DOM (визуализированными и, таким образом, созданными) элементами.
  • Присоедините обработчики событий.
  • Осмотрите дочерние элементы.
  • Настройте наблюдения на атрибуты.
  • Установите часы на прицеле.
Izhaki
источник
9
Если кто-то использует функцию ссылки (без предварительной ссылки или пост-ссылки), полезно знать, что она эквивалентна пост-ссылке.
Асаф Дэвид
15

Функция предварительной ссылки

pre-linkФункция каждой директивы вызывается всякий раз, когда создается новый связанный элемент.

Как показано ранее в разделе порядка компиляции, pre-linkфункции называются parent-then-child, тогда как post-linkфункции называютсяchild-then-parent .

pre-linkФункция используется редко, но может быть полезен в специальных ситуациях; например, когда дочерний контроллер регистрирует себя с родительским контроллером, но регистрация должна быть в parent-then-childмоде ( ngModelControllerделает вещи таким образом).

Не делайте:

  • Проверьте дочерние элементы (они еще не могут быть отображены, привязаны к области и т. Д.).
Izhaki
источник