9.8 KiB
Порядок обработки событий
События могут возникать не только по очереди, но и пачкой, по многу сразу. Возможно и такое, что во время обработки одного события возникают другие.
Здесь и далее, очень важно понимать, как браузер обычно работает с событиями и важные исключения из этого правила. Это мы и разберём в этой главе.
[cut]
Главный поток
В каждом окне выполняется только один главный поток, который занимается выполнением JavaScript, отрисовкой и работой с DOM.
Он выполняет команды последовательно и блокируется при выводе модальных окон, таких как alert
.
[smart header="Дополнительные потоки тоже есть"] Есть и другие, служебные потоки, например, для сетевых коммуникаций.
Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на alert
. Но управлять служебными потоками мы не можем.
[/smart]
[smart header="Web Workers"] Существует спецификация Web Workers, которая позволяет запускать дополнительные JavaScript-процессы(workers).
Они могут обмениваться сообщениями с главным процессом, но их переменные полностью независимы.
В частности, дополнительные процессы не имеют доступа к DOM, поэтому они полезны, преимущественно, при вычислениях, чтобы загрузить несколько ядер/процессоров одновременно. [/smart]
Очередь событий
Произошло одновременно несколько событий или во время работы одного случилось другое -- как главному потоку обработать это?
Если главный поток прямо сейчас занят, то он не может срочно выйти из середины одной функции и прыгнуть в другую. А потом третью. Отладка при этом могла бы превратиться в кошмар, потому что пришлось бы разбираться с совместным состоянием нескольких функций сразу.
Поэтому используется альтернативный подход.
Когда происходит событие, оно попадает в очередь.
Внутри браузера существует главный внутренний цикл, который проверяет очередь и обрабатывает события, запускает соответствующие обработчики и т.п.
Иногда события добавляются в очередь сразу пачкой.
Например, при клике на элементе генерируется несколько событий:
- Сначала `mousedown` -- нажата кнопка мыши.
- Затем `mouseup` -- кнопка мыши отпущена.
- Так как это было над одним элементом, то дополнительно генерируется `click`
В действии:
<!--+ autorun -->
<textarea rows="6" cols="40" id="area">Кликни меня
</textarea>
<script>
area.onmousedown = function(e) { this.value += "mousedown\n"; this.scrollTop = 1e9; };
area.onmouseup = function(e) { this.value += "mouseup\n"; this.scrollTop = 1e9; };
area.onclick = function(e) { this.value += "click\n"; this.scrollTop = 1e9; };
</script>
Таким образом, при нажатии кнопки мыши в очередь попадёт событие mousedown
, а при отпускании -- сразу два события: mouseup
и click
. Браузер сначала обработает первое, а потом -- второе.
При этом каждое событие из очереди обрабатывается полностью отдельно от других.
Вложенные (синхронные) события
В тех случаях, когда событие инициируется не посетителем, а кодом, то оно, как правило, обрабатывается синхронно, то есть прямо сейчас.
Рассмотрим в качестве примера событие onfocus
.
Пример: событие onfocus
Когда посетитель фокусируется на элементе, возникает событие onfocus
. Обычно оно происходит, когда посетитель кликает на поле ввода, например:
<!--+ run autorun -->
<p>При фокусе на поле оно изменит значение.</p>
<input type="text" onfocus="this.value = 'Фокус!'" value="Кликни меня">
Но ту же фокусировку можно вызвать и явно, вызовом метода elem.focus()
:
<!--+ run -->
<input type="text" id="elem" onfocus="this.value = 'Фокус!'">
<script>
*!*
// сфокусируется на input и вызовет обработчик onfocus
elem.focus();
*/!*
</script>
В главе мы познакомимся с этим событием подробнее, а пока -- нажмите на кнопку в примере ниже. При этом обработчик onclick
вызовет метод focus()
на текстовом поле text
.
Событие onfocus
, инициированное вызовом text.focus()
, будет обработано синхронно, прямо сейчас, до завершения onclick
.
<!--+ autorun -->
<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
.
Так ведут себя все браузеры, кроме IE.
В нём событие onfocus
-- всегда асинхронное, так что будет сначала полностью обработан клик, а потом -- фокус. В остальных -- фокус вызовется посередине клика. Попробуйте кликнуть в IE и в другом браузере, чтобы увидеть разницу.
Делаем события асинхронными через setTimeout(...,0)
А что, если мы хотим, чтобы сначала закончилась обработка onclick
, а потом уже произошла обработка onfocus
и связанные с ней действия?
Можно добиться и этого.
Один вариант -- просто переместить строку text.focus()
вниз кода обработчика.
Если это неудобно, можно запланировать text.focus()
чуть позже через setTimeout(..., 0)
, вот так
<!--+ autorun -->
<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 выполняется в едином потоке. Современные браузеры позволяют порождать подпроцессы Web Workers, они выполняются параллельно и могут отправлять/принимать сообщения, но не имеют доступа к DOM.
- Обычно события становятся в очередь и обрабатываются в порядке поступления, асинхронно, независимо друг от друга.
- Синхронными являются вложенные события, инициированные из кода.
- Чтобы сделать событие гарантированно асинхронным, используется вызов через `setTimeout(func, 0)`.