en.javascript.info/2-ui/2-events-and-interfaces/2-events-and-timing-depth/article.md
Ilya Kantor 87bf53d076 update
2014-11-16 01:40:20 +03:00

9.8 KiB
Raw Blame History

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

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

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

[cut]

Главный поток

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

Он выполняет команды последовательно и блокируется при выводе модальных окон, таких как alert.

[smart header="Дополнительные потоки тоже есть"] Есть и другие, служебные потоки, например, для сетевых коммуникаций.

Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на alert. Но управлять служебными потоками мы не можем. [/smart]

[smart header="Web Workers"] Существует спецификация Web Workers, которая позволяет запускать дополнительные JavaScript-процессы(workers).

Они могут обмениваться сообщениями с главным процессом, но их переменные полностью независимы.

В частности, дополнительные процессы не имеют доступа к DOM, поэтому они полезны, преимущественно, при вычислениях, чтобы загрузить несколько ядер/процессоров одновременно. [/smart]

Очередь событий

Произошло одновременно несколько событий или во время работы одного случилось другое -- как главному потоку обработать это?

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

Поэтому используется альтернативный подход.

Когда происходит событие, оно попадает в очередь.

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

Иногда события добавляются в очередь сразу пачкой.

Например, при клике на элементе генерируется несколько событий:

  1. Сначала `mousedown` -- нажата кнопка мыши.
  2. Затем `mouseup` -- кнопка мыши отпущена.
  3. Так как это было над одним элементом, то дополнительно генерируется `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)`.