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

11 KiB
Raw Blame History

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

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

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

[cut]

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

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

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

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

Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на `alert`. Но управлять служебными потоками мы не можем.
Существует спецификация <a href="http://www.w3.org/TR/workers/">Web Workers</a>, которая позволяет запускать дополнительные JavaScript-процессы(workers).

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

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

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

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

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

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

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

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

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

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

  1. Сначала mousedown -- нажата кнопка мыши.
  2. Затем mouseup -- кнопка мыши отпущена и, так как это было над одним элементом, то дополнительно генерируется click (два события сразу).
В действии:

```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. Обычно оно происходит, когда посетитель кликает на поле ввода, например:

<p>При фокусе на поле оно изменит значение.</p>
<input type="text" onfocus="this.value = 'Фокус!'" value="Кликни меня">

Но ту же фокусировку можно вызвать и явно, вызовом метода elem.focus():

<input type="text" id="elem" onfocus="this.value = 'Фокус!'">

<script>
*!*
  // сфокусируется на input и вызовет обработчик onfocus
  elem.focus();
*/!*
</script>

В главе info:focus-blur мы познакомимся с этим событием подробнее, а пока -- нажмите на кнопку в примере ниже.

При этом обработчик onclick вызовет метод focus() на текстовом поле text. Код обработчика onfocus, который при этом запустится, сработает синхронно, прямо сейчас, до завершения onclick.

<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() вниз кода обработчика onclick.

Если это неудобно, можно запланировать text.focus() чуть позже через setTimeout(..., 0), вот так

<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).

Отложенный вызов через setTimeout(func, 0) используется не только в событиях, а вообще -- всегда, когда мы хотим, чтобы некая функция func сработала после того, как текущий скрипт завершится.