en.javascript.info/archive/2-events-and-timing-depth/article.md
Ilya Kantor 4c08f02317 up
2017-03-02 03:24:47 +03:00

172 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Порядок обработки событий
События могут возникать не только по очереди, но и "пачкой" по много сразу. Возможно и такое, что во время обработки одного события возникают другие, например пока выполнялся код для `onclick` -- посетитель нажал кнопку на клавиатуре (событие `keydown`).
Здесь мы разберём, как браузер обычно работает с одновременно возникающими событиями и какие есть исключения из общего правила.
[cut]
## Главный поток
В каждом окне выполняется только один *главный* поток, который занимается выполнением JavaScript, отрисовкой и работой с DOM.
Он выполняет команды последовательно, может делать только одно дело одновременно и блокируется при выводе модальных окон, таких как `alert`.
```smart header="Дополнительные потоки тоже есть"
Есть и другие, служебные потоки, например, для сетевых коммуникаций.
Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на `alert`. Но управлять служебными потоками мы не можем.
```
```smart header="Web Workers"
Существует спецификация <a href="http://www.w3.org/TR/workers/">Web Workers</a>, которая позволяет запускать дополнительные JavaScript-процессы(workers).
Они могут обмениваться сообщениями с главным процессом, но у них свои переменные, и работают они также сами по себе.
Такие дополнительные процессы не имеют доступа к DOM, поэтому они полезны, преимущественно, при вычислениях, чтобы загрузить несколько ядер/процессоров одновременно.
```
## Очередь событий
Произошло одновременно несколько событий или во время работы одного случилось другое -- как главному потоку обработать это?
Если главный поток прямо сейчас занят, то он не может срочно выйти из середины одной функции и прыгнуть в другую. А потом третью. Отладка при этом могла бы превратиться в кошмар, потому что пришлось бы разбираться с совместным состоянием нескольких функций сразу.
Поэтому используется альтернативный подход.
**Когда происходит событие, оно попадает в очередь.**
Внутри браузера непрерывно работает "главный внутренний цикл", который следит за состоянием очереди и обрабатывает события, запускает соответствующие обработчики и т.п.
**Иногда события добавляются в очередь сразу пачкой.**
Например, при клике на элементе генерируется несколько событий:
1. Сначала `mousedown` -- нажата кнопка мыши.
2. Затем `mouseup` -- кнопка мыши отпущена и, так как это было над одним элементом, то дополнительно генерируется `click` (два события сразу).
````online
В действии:
```html autorun height=150 no-beautify
<textarea rows="8" cols="40" id="area">Кликни меня
</textarea>
<script>
area.onmousedown = function(e) { this.value += "mousedown\n"; this.scrollTop = this.scrollHeight; };
area.onmouseup = function(e) { this.value += "mouseup\n"; this.scrollTop = this.scrollHeight; };
area.onclick = function(e) { this.value += "click\n"; this.scrollTop = this.scrollHeight; };
</script>
```
````
Таким образом, при нажатии кнопки мыши в очередь попадёт событие `mousedown`, а при отпускании -- сразу два события: `mouseup` и `click`. Браузер обработает их строго одно за другим: `mousedown` -> `mouseup` -> `click`.
При этом каждое событие из очереди обрабатывается полностью отдельно от других.
## Вложенные (синхронные) события
Обычно возникающие события "становятся в очередь".
Но в тех случаях, когда событие инициируется не посетителем, а кодом, то оно, как правило, обрабатывается синхронно, то есть прямо сейчас.
Рассмотрим в качестве примера событие `onfocus`.
### Пример: событие onfocus
Когда посетитель фокусируется на элементе, возникает событие `onfocus`. Обычно оно происходит, когда посетитель кликает на поле ввода, например:
```html run height=80 autorun
<p>При фокусе на поле оно изменит значение.</p>
<input type="text" onfocus="this.value = 'Фокус!'" value="Кликни меня">
```
Но ту же фокусировку можно вызвать и явно, вызовом метода `elem.focus()`:
```html run
<input type="text" id="elem" onfocus="this.value = 'Фокус!'">
<script>
*!*
// сфокусируется на input и вызовет обработчик onfocus
elem.focus();
*/!*
</script>
```
В главе <info:focus-blur> мы познакомимся с этим событием подробнее, а пока -- нажмите на кнопку в примере ниже.
При этом обработчик `onclick` вызовет метод `focus()` на текстовом поле `text`. Код обработчика `onfocus`, который при этом запустится, сработает синхронно, прямо сейчас, до завершения `onclick`.
```html autorun height=80 no-beautify
<input type="button" id="button" value="Нажми меня">
<input type="text" id="text" size="60">
<script>
button.onclick = function() {
text.value += ' ->в onclick ';
text.focus(); // вызов инициирует событие onfocus
text.value += ' из onclick-> ';
};
text.onfocus = function() {
text.value += ' !focus! ';
};
</script>
```
При клике на кнопке в примере выше будет видно, что управление вошло в `onclick`, затем перешло в `onfocus`, затем вышло из `onclick`.
```warn header="Исключение в IE"
Так ведут себя все браузеры, кроме IE.
В нём событие `onfocus` -- всегда асинхронное, так что будет сначала полностью обработан клик, а потом -- фокус. В остальных -- фокус вызовется посередине клика. Попробуйте кликнуть в IE и в другом браузере, чтобы увидеть разницу.
```
## Делаем события асинхронными через setTimeout(...,0)
А что, если мы хотим, чтобы *сначала* закончилась обработка `onclick`, а потом уже произошла обработка `onfocus` и связанные с ней действия?
Можно добиться и этого.
Один вариант -- просто переместить строку `text.focus()` вниз кода обработчика `onclick`.
Если это неудобно, можно запланировать `text.focus()` чуть позже через `setTimeout(..., 0)`, вот так
```html autorun height=80
<input type="button" id="button" value="Нажми меня">
<input type="text" id="text" size="60">
<script>
button.onclick = function() {
text.value += ' ->в onclick ';
*!*
setTimeout(function() {
text.focus(); // сработает после onclick
}, 0);
*/!*
text.value += ' из onclick-> ';
};
text.onfocus = function() {
text.value += ' !focus! ';
};
</script>
```
Такой вызов обеспечит фокусировку через минимальный "тик" таймера, по стандарту равный 4 мс. Обычно такая задержка не играет роли, а необходимую асинхронность мы получили.
## Итого
- JavaScript выполняется в едином потоке. Современные браузеры позволяют порождать подпроцессы <a href="http://www.w3.org/TR/workers/">Web Workers</a>, они выполняются параллельно и могут отправлять/принимать сообщения, но не имеют доступа к DOM.
- Обычно события становятся в очередь и обрабатываются в порядке поступления, асинхронно, независимо друг от друга.
- Синхронными являются вложенные события, инициированные из кода.
- Чтобы сделать событие гарантированно асинхронным, используется вызов через `setTimeout(func, 0)`.
Отложенный вызов через `setTimeout(func, 0)` используется не только в событиях, а вообще -- всегда, когда мы хотим, чтобы некая функция `func` сработала после того, как текущий скрипт завершится.