Разница между действиями 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
?
Базовый обзор:
- Событие DOM создано
- Все собственные прослушиватели событий DOM запускаются, начиная с целевого узла и поднимаясь вверх по дереву - если только один из них не останавливает распространение
- Все слушатели действий Ember запускаются, начиная с целевого узла и поднимаясь по дереву - если только один из них не останавливает распространение.
Полный (er) обзор объяснен в этой супер-радикальной блок-схеме, которую я сделал (детище упомянутого ранее кухонного танца).
Совет от профессионала: все цвета в этой диаграмме соответствуют цветам, используемым для разных слушателей событий в Ember Twiddle demo, так что перекрестные ссылки на досуге!
Типы слушателей событий в Ember
Как узнать, используете ли вы прослушиватель событий DOM или обработчик действий Ember? Когда ваше действие получит доступ к исходному событию DOM? Может ли ваше действие предотвратить запуск других действий?
Есть три основных способа добавления слушателей в Ember:
- Добавление прослушивателя действий 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, а дочернее действие позже переключает его обратно на закрытие.
В этом примере события будут запускаться в следующем порядке (при условии, что ни один обработчик не остановит распространение):
- События DOM на
<a>
- События DOM на
<div class="tour-tooltip">
- События DOM на
<div class="cool-new-button">
(открывает The Thing) - Действия Ember на
<a>
(закрывает The Thing) - Действия Ember на
<div class="tour-tooltip">
- Действия 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, чтобы мы вместе учились и создавали отличные продукты!