- Главный поток JavaScript
- Главный цикл
- Использование
setTimeout(func, 0) - Вложенные (синхронные) события
- Блокировка потока, синхронные вызовы
- Итого
Внутри браузеры построены на событиях.
Большинство действий — загрузка файла, клик посетителя и другие, создают событие, которое добавляется в очередь. Когда это возможно, браузер берёт событие из очереди и обрабатывает его. Многие, хотя и не все события доступны из JavaScript.
Главный поток JavaScript
В каждом окне выполняется только один главный поток, который занимается выполнением JavaScript, отрисовкой и работой с DOM.
Некоторые действия, такие как отображение, загрузка файлов, могут управляться отдельными потоками, поэтому, скажем, скачивание файлов может продолжаться пока основной поток блокирован командой alert.
У этой однопоточности есть очевидное следствие.
В один момент времени выполняется только строка JavaScript-кода и, соответственно, только один обработчик события.
Неважно, сколько событий произошло одновременно, они становятся в очередь и будут обработаны одно за другим.. Хотя, как мы увидим дальше, есть и исключения ![]()
Существует спецификация Web Workers, которая позволяет запускать дополнительные JavaScript-потоки(workers).
Они могут обмениваться сообщениями с главным потоком, но у них своя область данных, поэтому правильнее было бы назвать их «процессами», чем «потоками».
В частности, они не имеют доступа к DOM и полезны, преимущественно, при вычислениях, т.к. позволяют использовать несколько процессоров одновременно.
Основной поток занимается выполнением JavaScript, отрисовкой, обрабатывает события.
Существуют еще независимые потоки, например, для сетевых коммуникаций. Так что пока основной поток стоит, могут качаться файлы.
Главный цикл
Большинство событий — асинхронны.
Когда происходит асинхронное событие, оно попадает в очередь.
Внутри браузера существует главный внутренний цикл, который проверяет очередь и обрабатывает события, запускает соответствующие обработчики и т.п.
Например, если в момент, когда браузер обрабатывает событие click, где-то произошло другое событие (например, загрузился скрипт), это новое событие добавляется в очередь.
По завершении onclick, браузер возьмёт из очереди следующее событие и обработает его.
Несколько событий могут быть инициированы и добавлены в очередь одновременно.
Например, событие mousedown, при mouseup на том же элементе, генерирует событие click. Оба этих события mouseup и click происходят одновременно и добавляются в очередь вместе.
При нажатии на элемент формы, одновременно с mousedown происходит событие focus. Есть и другие подобные ситуации.
Функции setTimeout/setInterval также добавляют запуск функций в очередь событий, если браузер занят.
Использование setTimeout(func, 0)
Когда функция setTimeout получает 0 в качестве последнего аргумента, она стремится выполнить func как можно скорее. Но сразу же она сделать это не может.
Максимум возможного для этой функции — добавить вызов func в очередь событий на ближайшем «тике» таймера.
Далее мы рассмотрим случаи, когда такой трюк может быть полезен.
Позволить родителю обработать событие
Как мы знаем, обычно событие сначала происходит на потомке, а потом всплывает к родителям.
Что же делать, если дочерний элемент хочет обработать событие после того, как на событие среагирует родитель?
Например, на document.onkeydown стоит общий обработчик горячих клавиш, а на элементе elem.onkeydown стоит какой-то частный обработчик ввода, который должен срабатывать после горячих главиш
Есть фаза захвата, но она не поддерживается в IE<9. Кроме того, что если часть действий нужно сделать тут же, а до родителя, а часть — после?
Решение — отложить обработку elem.onkeydown через setTimeout(.., 0), и тем самым дать событию всплыть, а уже потом — продолжить обработку.
Например:
<input type="button" value="Нажми меня">
<script>
var elem = document.body.children[0];
elem.onclick = function() {
function handle() {
elem.value +=' -> input';
}
*!*
setTimeout(handle, 0); // отложить обработку
*/!*
}
document.onclick = function() {
input.value += ' -> document';
}
</script>
Позволить действию браузера завершиться
Ряд событий, например keydown, происходят до того, как браузер сделает соответствующее действие (добавит символ в поле ввода). Это здорово, так как даёт возможность отменить действие браузера.
С другой стороны, на момент срабатывания обработчика — символа ещё нет. А что, если он нужен?
В примере ниже обработчик приводит значение к верхнему регистру при каждом нажатии. Попробуйте набрать что-нибудь:
<input id="my" type="text" placeholder="keypress">
<script>
document.getElementById('my').onkeypress = function(event) {
this.value = this.value.toUpperCase();
};
</script>
Видите? Не работает! Значение увеличивается, кроме последнего символа, т.к. браузер добавит его после того, как обработка keypress завершится.
keyupМы, конечно, могли бы использовать событие keyup, т.к. оно происходит после ввода. Но в этом случае символ будет изначально показан маленьким, и поменяет регистр при отпускании клавиши.
Это выглядит странно, попробуйте:
<input id="my" type="text" placeholder="keyup">
<script>
document.getElementById('my').onkeyup = function(event) {
this.value = this.value.toUpperCase();
};
</script>
Итак, что делать, если обработчик хочет сработать после действия браузера?
Решение — отложить обработку через setTimeout(.., 0):
<input id="my" type="text" placeholder="keypress+0">
<script>
document.getElementById('my').onkeypress = function() {
var self = this;
*!*
function handle() {
self.value = self.value.toUpperCase()
}
setTimeout(handle, 0);
*/!*
};
</script>
Тогда обработчик запустится после добавления символа, но достаточно скоро, чтобы посетитель не заметил задержку.
Вложенные (синхронные) события
Есть методы, которые инициируют событие при вызове, например elem.focus(). Такие события обрабатываются синхронно, то есть поток выполнения переходит в их обработчик тут же.
Пример: событие onfocus
Нажмите на кнопку в примере ниже. Вы увидите, что обработчик onfocus не ждёт завершения onclick, а выполняется тут же.
<input type="button" value="Нажми меня">
<input type="text" size="60">
<script>
var button = document.body.children[0];
var text = document.body.children[1];
button.onclick = function() {
text.value += ' ->в onclick ';
text.focus()
text.value += ' из onclick-> '
}
text.onfocus = function() {
text.value += ' !focus! ';
}
</script>
Пример: события изменения DOM
В примере ниже, обработчик onclick изменяет атрибута ссылки <a>. Это изменение генерирует событие propertychange в IE и DOMAttrModified в Firefox/Opera.
Событие является вложенным, поэтому будет обработано тут же, до окончания onclick:
(Пример не будет работать в Chrome/Safari, т.к. эти браузеры не поддерживают DOMAttrModified).
<a href="#">Нажми меня!</a>
<script>
var a = document.body.children[0];
a.onclick = function() {
alert('-> onlick');
// каждый клик ставим новое значение, чтобы было событие изменения
this.setAttribute('href', new Date);
alert('onclick ->');
return false;
}
function onpropertychange() {
alert('property change!');
}
if (a.onpropertychange === null) { // не undefined, значит поддержка есть
a.attachEvent('onpropertychange', onpropertychange); // (IE)
} else { // все, кроме и Chrome/Safari
a.addEventListener('DOMAttrModified', onpropertychange, false);
}
</script>
Порядок обработки:
- Вход в
onclick: вывод<-- onclick. - Атрибут изменён, тут же срабатывает синхронное событие, его обработчик выводит
'property change!. - Управление возвращается в
onclick, выводитсяonclick ->.
Синхронно также обрабатываются события, созданные браузерными методами dispatchEvent/fireEvent. Эти методы позволяют генерировать браузерное событие из JavaScript. Они редко используются в реальной жизни.
Откладываем вложенные события через setTimeout(..,0)
Как правило, обработчики событий вызываются по очереди. Мы предполагаем, что текущий обработчик завершится перед тем, как начнется другой.
Вложенные события ломают это правило и смешивают обработку, что может создать неудобства.
Можно, конечно, перенести вызов text.focus() в конец обработчика onclick, но это значит — изменить структуру кода.
Альтернатива — обернуть действие, вызывающее событие (text.focus()) в setTimeout(.., 0).
button.onclick = function() {
...
setTimeout(function() { text.focus(); }, 0);
...
}
Блокировка потока, синхронные вызовы
Синхронные вызовы, такие как alert, блокируют основной поток JavaScript. Большинство действий, включая обработку таймеров, при этом останавливаются.
Например, запустите пример ниже. В iframe начнётся анимация по setInterval. При нажатии на кнопку, она остановится.
<div style="height:20px;width:0px;background-color:green"></div>
<script>
var timer = setInterval(function() {
var style = document.getElementsByTagName('div')[0].style;
style.width = (parseInt(style.width)+2)%400 + 'px';
}, 30);
</script>
<input type="button" onclick="alert('Привет!')" value="alert('Привет в iframe!')">
<input type="button" onclick="clearInterval(timer)" value="clearInterval(timer)">
iframe в OperaКак правило, iframe'ы работают в одном потоке с основной страницей. Исключением является Opera.
Запустите пример выше в Opera и нажмите на кнопку в основном окне. Анимация в iframe продолжится!
Итого
- JavaScript выполняется в едином потоке. Современные браузеры позволяют порождать подпроцессы Web Workers, они выполняются параллельно и могут отправлять/принимать сообщения, но не имеют доступа к DOM.
- Большинство событий — асинхронные. Синхронными являются вложенные события, инициированные из кода.
-
Трюк с
setTimeout(func, 0)можно применить в обработчиках:- Чтобы дать родителю отработать.
- Чтобы дать браузеру отрагировать первому.
- Чтобы сделать вложенное событие асинхронным.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.