--- title: Модель конкурентності та цикл подій slug: Web/JavaScript/EventLoop tags: - JavaScript - обробники подій - події - цикл подій - черга translation_of: Web/JavaScript/EventLoop ---
JavaScript має модель одночасності, що базується на циклі подій, який є відповідальним за виконання коду, збір та обробку подій та виконання підзадач з черги. Ця модель доволі сильно відрізняється від моделей у інших мовах, таких як С та Java.
Наступні розділи пояснють теоретичну модель. Сучасні імплементації JavaScript реалізують і значно оптимізують описану семантику.
Виклики функцій утворюють стек фреймів (frames).
function foo(b) { var a = 10; return a + b + 11; } function bar(x) { var y = 3; return foo(x * y); } console.log(bar(7)); // вертає 42
Коли викликається bar
, утворюється перший фрейм, який містить аргументи та локальні змінні функції bar
. Коли bar
викликає foo
, створюється другий фрейм та розміщується над першим, він містить аргументи та локальні змінні функції foo
. Коли foo
повертає значення, верхній фрейм виштовхується зі стеку (залишаючи лише фрейм виклику bar
). Коли bar
повертає значення, стек стає порожнім.
Об'єкти розподіляються у купі, яка є лише назвою для позначення великої (здебільшого не структурованої) області пам'яті.
Процес виконання JavaScript використовує чергу повідомлень, яка є списком повідомлень, що мають бути опрацьовані. Кожне повідомлення має пов'язану функцію, яка викликається для обробки цього повідомлення
В певний момент {{anch("Цикл_подій", "циклу подій")}} процес виконання починає обробку повідомлень з черги, починаючи з найстаршого. Для цього повідомлення видаляється з черги, а його пов'язана функція викликається з повідомленням в якості вхідного параметра. Як завжди, виклик функції створює новий фрейм стеку для цієї функції.
Опрацювання функцій продовжується, доки стек знову не стане порожнім. Тоді цикл подій опрацює наступне повідомлення у черзі (якщо воно є).
Цикл подій отримав свою назву через те, як він зазвичай реалізується. Як правило, це схоже на:
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage
синхронно чекає на прибуття повідомлення (якщо воно вже не надійшло і не чекає на обробку).
Кожне повідомлення обробляється до завершення, перш, ніж обробляти будь-яке інше повідомлення.
Це надає деякі приємні властивості для вашої програми, в тому числі той факт, що, коли виконується функція, вона не може бути попередньо вилучена і виконається до кінця перш, ніж буде запущено будь-який інший код (і зможе змінювати дані, якими користується функція). Це відрізняється, наприклад, від C, де, якщо функція виконується у потоці, її можна зупинити в будь-якій точці, щоб запустити інший код в іншому потоці.
Зворотним боком цієї моделі є те, що, якщо обробка повідомлення займає надто багато часу, веб-застосунок не може обробити взаємодії користувача, як-от натискання чи прокручування. Веб-переглядач пом'якшує це діалоговим вікном "a script is taking too long to run" (виконання сценарію займає забагато часу). Гарною практикою є робити обробку повідомлень короткою і, за можливості, розбивати одне повідомлення на декілька.
У веб-переглядачах повідомлення додаються щоразу, коли виникає подія, до якої приєднаний прослуховувач подій. Якщо прослуховувача немає, подія втрачається. Отже, натискання на елементі з обробником подій натискання додасть повідомлення, так само з будь-якою іншою подією.
Функція setTimeout
викликається з двома аргументами: повідомлення, що додається до черги, та значення часу (необов'язкове; за замовчуванням 0
). Значення часу відображає (мінімальну) затримку, після якої повідомлення буде, власне, додане до черги. Якщо в черзі немає інших повідомлень, це повідомлення буде оброблене одразу після затримки. Однак, якщо там є повідомлення, повідомленню setTimeout
доведеться зачекати, доки не будуть оброблені інші повідомлення. З цієї причини другий аргумент вказує мінімальний, а не гарантований час.
Ось приклад, який демонструє цю концепцію (setTimeout
не виконується негайно після того, як його таймер завершився):
const s = new Date().getSeconds(); setTimeout(function() { // виводить "2", тобто, функція зворотного виклику не запустилась одразу через 500 мілісекунд. console.log("Функція запустилась через " + (new Date().getSeconds() - s) + " секунд"); }, 500) while (true) { if (new Date().getSeconds() - s >= 2) { console.log("Добре, виконувалось 2 секунди") break; } }
Нульова затримка насправді не означає, що зворотній виклик запуститься через нуль мілісекунд. Виклик {{domxref ("WindowTimers.setTimeout", "setTimeout")}} із затримкою в 0
(нуль) мілісекунд не виконує функцію зворотного виклику після заданого інтервалу.
Виконання залежить від кількості задач, що чекають у черзі. У наведеному нижче прикладі повідомлення "це просто повідомлення"
буде написане у консоль раніше, ніж буде оброблене повідомлення у зворотному виклику, оскільки затримка - це мінімальний час, необхідний для обробки запиту (а не гарантований час).
Загалом, setTimeout
має чекати, доки виконається весь код для повідомлень у черзі, незважаючи на те, що ви вказали певний часовий ліміт для своєї функції setTimeout
.
(function() { console.log('це початок'); setTimeout(function cb() { console.log('Зворотний виклик 1: це повідомлення зворотного виклику'); }); console.log('це просто повідомлення'); setTimeout(function cb1() { console.log('Зворотний виклик 2: це повідомлення зворотного виклику'); }, 0); console.log('це кінець'); })(); // "це початок" // "це просто повідомлення" // "це кінець" // "Зворотний виклик 1: це повідомлення зворотного виклику" // "Зворотний виклик 2: це повідомлення зворотного виклику"
Веб-виконавець або iframe перехресного походження має свій стек, купу та чергу повідомлень. Два різних процеси виконання можуть спілкуватися надсиланням повідомлень за допомогою методу postMessage
. Цей метод додає повідомлення до іншого процесу виконання, якщо останній прослуховує події message
.
Дуже цікавою властивістю моделі циклу подій є те, що JavaScript, на відміну від багатьох інших мов, ніколи не блокує. Управління введенням/виводом зазвичай здійснюється за допомогою подій та зворотних викликів, тому, коли програма чекає на результат запиту IndexedDB чи запиту XHR, вона може опрацьовувати інші події, такі як введення даних користувачем.
Існують спадкові винятки, такі як alert
або синхронний XHR, але вважається гарною практикою їх уникати. Будьте обережні: існують винятки з винятку (але зазвичай це помилки реалізації, а не що-небудь інше).
Специфікація |
---|
{{SpecName('HTML WHATWG', 'webappapis.html#event-loops', 'Event loops')}} |
Node.js Event Loop |