Разница между действиями Ember и событиями DOM и почему это важно, плюс действительно классная блок-схема.

Внимание, мы переехали! Если вы хотите и дальше следить за последними техническими новостями Square, посетите наш новый дом https://developer.squareup.com/blog.

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

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

До: При нажатии на «Следующий шаг обзора» открывается The Thing.

<div class="cool-new-button" {{action "toggleTheThing"}}>
  {{#if shouldShowThisTooltip}}
    <div class="tour-tooltip">
      This button can open and close The Thing!
    
      <a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
        Next Step of the Tour
      </a>
    </div>
  {{/if}}
</div>

После: нажатие на «Следующий шаг тура» открыло The Thing и сразу же закрыло его - так быстро, что казалось, что ничего не произошло.

<div class="cool-new-button" onClick={{action "toggleTheThing"}}>
  {{#if shouldShowThisTooltip}}
    <div class="tour-tooltip">
      This button can open and close The Thing!
    
      <a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
        Next Step of the Tour
      </a>
    </div>
  {{/if}}
</div>

Единственное изменение: <div {{action "toggleTheThing"}}> стал<div onClick={{action "toggleTheThing"}}>

Я: а что?

Итак, я пошел на отладочные гонки и, немного поработав, пришел к выводу, что, несмотря на их внешнее сходство, {{action "foo"}} и onClick={{action "foo"}} представляют собой совершенно разные способы прослушивания щелчков.

Использование onClick={{action "foo"}} прослушивает события DOM, которые браузер отправляет напрямую, тогда как {{action "foo"}} прослушивает действия, запускаемые Ember в ответ на события браузера. Я мог сказать, что эти два типа слушателей событий слегка различаются по поведению, но еще не мог сформулировать что или почему.

Я понял, что просто прикоснулся к тому, как обрабатываются события в Ember, и мне нужно было еще многому научиться.

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

Вот что я узнал!

Интерактивная демонстрация: события в Ember

Если вы визуальный человек и хотите заполучить код, чтобы самостоятельно изучить эти идеи, я создал удобную демонстрацию Ember Twiddle, которая объясняет три распространенных способа прослушивания событий в Ember и способы их взаимодействия.

Если вы предпочитаете получить четкое представление обо всем, прежде чем опробовать демоверсию, продолжайте читать!

Основы

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

DOM: объектная модель документа, API, который описывает, как работают веб-страницы - как HTML отображается на странице, какие события происходят, когда пользователи взаимодействуют с этой страницей, и многое другое. Такие браузеры, как Chrome, Firefox и Safari, реализуют этот API для отображения веб-сайтов. (Для получения дополнительной информации ознакомьтесь с этой документацией DOM.)

Узел DOM: отдельный элемент в DOM. Например, <div> - это единственный узел DOM. Узлы могут иметь родителей и детей:

<div id="parent">                  <!-- This is a node. -->
  <button id="child"></button>     <!-- This is also a node. -->
</div>

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

Вы можете добавить прослушиватель событий к любому узлу DOM, который должен что-то делать, когда с ним происходит определенное событие; например, вам может быть небезразлично, когда нажимается кнопка или когда пользователь вводит текст в текстовое поле. Слушатель - это функция JavaScript, которая вызывается браузером всякий раз, когда это событие происходит на указанном узле. При вызове слушателю передается объект Event с соответствующей информацией.

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

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

Порядок событий в Ember

Когда пользователь нажимает на вашу новую причудливую кнопку, как именно запускается функция, определенная в вашем компоненте? Что произойдет, если предыдущее действие вызывает stopPropagation() для события или другое действие установило bubbles=false?

Базовый обзор:

  1. Событие DOM создано
  2. Все собственные прослушиватели событий DOM запускаются, начиная с целевого узла и поднимаясь вверх по дереву - если только один из них не останавливает распространение
  3. Все слушатели действий Ember запускаются, начиная с целевого узла и поднимаясь по дереву - если только один из них не останавливает распространение.

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

Совет от профессионала: все цвета в этой диаграмме соответствуют цветам, используемым для разных слушателей событий в Ember Twiddle demo, так что перекрестные ссылки на досуге!

Типы слушателей событий в Ember

Как узнать, используете ли вы прослушиватель событий DOM или обработчик действий Ember? Когда ваше действие получит доступ к исходному событию DOM? Может ли ваше действие предотвратить запуск других действий?

Есть три основных способа добавления слушателей в Ember:

  1. Добавление прослушивателя действий Ember к компоненту с атрибутом имени события.
{{some-component click=(action “handleClick”)}}

2. Добавление слушателя действий Ember к узлу DOM путем изменения элемента с помощью помощника action.

<div {{action "handleClick"}}></div>
<div {{action "handleDoubleClick" on="doubleClick"}}></div>

3. Добавление прослушивателя событий DOM к узлу DOM с помощью атрибута HTML события.

<div onclick={{action "handleClick"}}></div>

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

1. Атрибуты событий компонента

Тип слушателя: действие Ember.

Доступ к исходному событию DOM: Да

Поддерживаемые события: определены Ember.Component

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

* Вы не можете передавать bubbles=false как часть хэша действия закрытия (например, click=(action "foo" bubbles=false)). Если вы хотите остановить распространение, вы должны вызвать event.stopPropagation() в своем обработчике. См. Эту проблему GitHub или документацию для помощника действий для получения дополнительной информации.

2. Модификаторы элементов

Тип слушателя: действие Ember.

Доступ к исходному событию DOM: нет

Поддерживаемые события: определены Ember.Templates.helpers

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

* Действие этого типа не имеет доступа к исходному событию DOM, поэтому оно не может вызывать event.stopPropagation(). Если вы хотите остановить распространение, вы должны установить bubbles=false в помощнике действий.

3. Атрибуты событий DOM

Тип прослушивателя: событие DOM

Доступ к исходному событию DOM: Да

Поддерживаемые события: определены DOM API

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

* Установка bubbles=false в помощнике действий на самом деле не останавливает распространение событий DOM - это абстракция, специфичная для Ember. Если вы хотите остановить распространение, вы должны вызвать event.stopPropagation() в своем обработчике.

** Здесь это становится еще более странным - вызов event.stopPropagation() предотвратит запуск любых действий Ember из-за этого события - даже дочерних действий, что полностью противоречит нормальному порядку распространения DOM но которые вы можете увидеть в действии в этом Ember Twiddle. Почему? Это потому, что вызов stopPropagation в этот момент предотвращает всплытие события на специальный узел <div id="root">, который является родительским узлом всего внутри приложения Ember. Обработчик этого узла запускает процесс запуска действий Ember - см. Блок-схему выше, если это все еще сбивает с толку!

Уроки и рекомендации

Все это может быть интересно, но как это связано с ошибкой, которая изначально привела меня в эту кроличью нору?

И что это значит для вас, когда вы обрабатываете события в Ember?

Давайте еще раз посмотрим на этот пример кода с ошибками:

<div class="cool-new-button" onClick={{action "toggleTheThing"}}>
{{#if shouldShowThisTooltip}}
    <div class="tour-tooltip">
      This button can open and close The Thing!
    
      <a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
        Next Step of the Tour
      </a>
    </div>
  {{/if}}
</div>

У нас есть дочерний узел, который использует обработчик действий Ember для toggleTheThingAndAdvanceToTheNextStep. Когда он обрабатывает событие Ember, он останавливает запуск любых других событий Ember (спасибо bubbles=false). Это кажется, как будто это должно предотвратить выполнение родительского действия.

Однако перед вызовом дочернего обработчика действий вызывается прослушиватель событий DOM (toggleTheThing) его родительского узла. У ребенка еще не было возможности остановить размножение. Родительское действие переключает The Thing open, а дочернее действие позже переключает его обратно на закрытие.

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

  1. События DOM на <a>
  2. События DOM на <div class="tour-tooltip">
  3. События DOM на <div class="cool-new-button"> (открывает The Thing)
  4. Действия Ember на <a> (закрывает The Thing)
  5. Действия Ember на <div class="tour-tooltip">
  6. Действия Ember на <div class="cool-new-button">

Выводы

  • События DOM всегда запускаются перед действиями Ember.
  • Присоединение действий непосредственно к атрибутам событий DOM (например, onclick) напрямую использует API событий DOM браузера.
  • Присоединение действий к атрибутам Ember (например, атрибут click компонента или изменение элемента с помощью action помощника) использует API действий Ember, абстракцию поверх API событий DOM.
  • Вы можете использовать bubbles=false, чтобы предотвратить распространение события только, если вы используете помощник шаблона действия в обычной форме, а не в форме закрытия (например, {{action "foo" bubbles=false}} работает, а click=(action "foo" bubbles=false) - нет).
  • Вы можете использовать event.stopPropagation(), чтобы предотвратить распространение события только, если вы используете обработчик, имеющий доступ к исходному событию DOM - либо с помощью атрибута события HTML, например onclick, либо с помощью атрибута события компонента, например click.
  • Вызов event.stopPropagation() в обработчике событий DOM остановит запуск любых действий Ember из-за этого события - рискованное дело!
  • Для согласованности и предотвращения мелких ошибок я рекомендую всегда использовать действия Ember над событиями DOM.

Одно возможное исключение для постоянного использования действий Ember: если вы хотите при желании добавить событие в узел DOM. В настоящее время хелперы шаблонов Ember не поддерживают изменение элементов с помощью встроенного условия:

<div {{if shouldRespondToClick (action "handleClick")}}></div>

Поскольку это недопустимый Handlebars, вам нужно сделать следующее:

<div onClick={{if shouldRespondToClick (action "handleClick")}}</div>

В качестве альтернативы вы можете выполнить эту проверку логики в обработчике действий и вернуться раньше: if (!this.get('shouldRespondToClick')) { return; }. Однако ваш элемент будет иметь стиль, связанный с возможностью нажатия, поэтому вам также может потребоваться добавить стиль, который устанавливает cursor: default.

Это большая работа, чтобы события были более последовательными; Вам решать, какой подход вы предпочитаете.

Главный вывод: теперь вы знаете, как работают события в Ember! и можете принимать обоснованные решения и уверенно выполнять отладку.

Начни танцевать на кухне, или, говоря словами моего любимого смайлика Slack, : corgi: on.

P.S. Вы любите разбираться в программном обеспечении и узнавать о технологиях? Вам следует присоединиться к нам в Square, чтобы мы вместе учились и создавали отличные продукты!