1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
---
title: Параллельная модель и цикл событий.
slug: Web/JavaScript/EventLoop
tags:
- Руководство
translation_of: Web/JavaScript/EventLoop
---
<p>{{JsSidebar("Advanced")}}</p>
<p>Параллелизм/Многопоточность в JavaScript работает за счёт цикла событий (<strong>event loop</strong>), который отвечает за выполнение кода, сбора и обработки событий и выполнения под-задач из очереди (<strong>queued sub-tasks</strong>). Эта модель весьма отличается от других языков программирования, таких как C и Java.</p>
<h2 id="Концепция_жизненного_цикла">Концепция жизненного цикла</h2>
<p>В следующей секции объясняется теоретическая модель. Современные JavaScript движки внедряют/имплементируют и существенно оптимизируют этот процесс.</p>
<h3 id="Визуальное_представление">Визуальное представление</h3>
<p style="text-align: center;"><img alt="Stack, heap, queue" src="/files/4617/default.svg" style="height: 270px; width: 294px;"></p>
<p style="text-align: center;">Для лучшего <strong>визуального</strong> представления работы <strong>Event loop</strong>, Вы можете ознакомиться с данным видео: <a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=389s">https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=389s</a></p>
<h3 id="Стек">Стек</h3>
<p>Вызов любой функции создает контекст выполнения (<a href="https://wiki.developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth#JavaScript_execution_contexts">Execution Context</a>). При вызове вложенной функции создается новый контекст, а старый сохраняется в специальной структуре данных - стеке вызовов (Call Stack).</p>
<pre class="brush: js">function f(b) {
var a = 12;
return a + b + 35;
}
function g(x) {
var m = 4;
return f(m * x);
}
g(21);
</pre>
<p>Когда вызывается функция <code>g</code>, создаётся первый контекст выполнения, содержащий аргументы функции <code>g</code> и локальные переменные. Когда <code>g</code> вызывает <code>f</code>, создаётся второй контекст с аргументами <code>f</code> и её локальными переменными. И этот контекст выполнения <code>f</code> помещается в стек вызовов выше первого. Когда <code>f</code> возвращает результат, верхний элемент из стека удаляется. Когда <code>g</code> возвращает результат, ее контекст также удалится, и стек становится пустым.</p>
<h3 id="Куча">Куча</h3>
<p>Объекты размещаются в <a href="https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C)">куче</a>. Куча — это просто имя для обозначения большой неструктурированной области памяти.</p>
<h3 id="Очередь">Очередь</h3>
<p>Среда выполнения JavaScript содержит очередь задач. Эта очередь — список задач, подлежащих обработке. Каждая задача ассоциируется с некоторой функцией, которая будет вызвана, чтобы обработать эту задачу.</p>
<p>Когда стек полностью освобождается, самая первая задача извлекается из очереди и обрабатывается. Обработка задачи состоит в вызове ассоциированной с ней функции с параметрами, записанными в этой задаче. Как обычно, вызов функции создаёт новый контекст выполнения и заносится в стек вызовов.</p>
<p>Обработка задачи заканчивается, когда стек снова становится пустым. Следующая задача извлекается из очереди и начинается её обработка.</p>
<h2 id="Цикл_событий">Цикл событий</h2>
<p>Модель событийного цикла (<code>event loop</code>) называется так потому, что отслеживает новые события в цикле:</p>
<pre class="brush: js">while(queue.waitForMessage()){
queue.processNextMessage();
}</pre>
<p><code>queue.waitForMessage</code> ожидает поступления задач, если очередь пуста.</p>
<h3 id="Запуск_до_завершения">Запуск до завершения</h3>
<p>Каждая задача выполняется полностью, прежде чем начнет обрабатываться следующая. Благодаря этому мы точно знаем: когда выполняется текущая функция – она не может быть приостановлена и будет целиком завершена до начала выполнения другого кода (который может изменить данные, с которыми работает текущая функция). Это отличает JavaScript от такого языка программирования как C. Поскольку в С функция, запущенная в отдельном потоке, в любой момент может быть остановлена, чтобы выполнить какой-то другой код в другом потоке.</p>
<p>У данного подхода есть и минусы. Если задача занимает слишком много времени, то веб-приложение не может обрабатывать действия пользователя в это время (например, скролл или клик). Браузер старается смягчить проблему и выводит сообщение <em>"скрипт выполняется слишком долго" ("a script is taking too long to run")</em> и предлагает остановить его. Хорошей практикой является создание задач, которые исполняются быстро, и если возможно, разбиение одной задачи на несколько мелких.</p>
<h3 id="Добавление_событий_в_очередь">Добавление событий в очередь</h3>
<p>В браузерах события добавляются в очередь в любое время, если событие произошло, а так же если у него есть обработчик. В случае, если обработчика нет – событие потеряно. Так, клик по элементу, имеющему обработчик события по событию <code>click </code>, добавит событие в очередь, а если обработчика нет – то и событие в очередь не попадет.</p>
<p>Вызов <a href="/ru/docs/Web/API/WindowTimers/setTimeout" title="/en-US/docs/window.setTimeout">setTimeout</a> добавит событие в очередь по прошествии времени, указанного во втором аргументе вызова. Если очередь событий на тот момент будет пуста, то событие обработается сразу же, в противном случае событию функции <code>setTimeout</code> придется ожидать завершения обработки остальных событий в очереди. Именно поэтому второй аргумент <code>setTimeout</code> корректно считать не временем, через которое выполнится функция из первого аргумента, а минимальное время, через которое она сможет выполниться.</p>
<h3 id="Нулевые_задержки">Нулевые задержки</h3>
<p>Нулевая задержка не дает гарантии, что обработчик выполнится через ноль миллисекунд. Вызов {{domxref("WindowTimers.setTimeout", "setTimeout")}} с аргументом 0 (ноль) не завершится за указанное время. Выполнение зависит от количества ожидающих задач в очереди. Например, сообщение ''this is just a message'' из примера ниже будет выведено на консоль раньше, чем произойдет выполнение обработчика <em>cb1</em>. Это произойдет, потому что задержка – это минимальное время, которое требуется среде выполнения на обработку запроса.</p>
<pre class="brush: js">(function () {
console.log('this is the start');
setTimeout(function cb() {
console.log('this is a msg from call back');
});
console.log('this is just a message');
setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0);
console.log('this is the end');
})();
// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"</pre>
<h3 id="Связь_нескольких_потоков_между_собой">Связь нескольких потоков между собой</h3>
<p>Web Worker или кросс-доменный фрейм имеют свой собственный стек, кучу и очередь событий. Два отдельных событийных потока могут связываться друг с другом, только через отправку сообщений с помощью метода <code><a href="/en-US/docs/DOM/window.postMessage" title="/en-US/docs/DOM/window.postMessage">postMessage</a>. </code>Этот метод добавляет <code style="font-style: normal;">сообщение</code> в очередь другого, если он конечно принимает их.</p>
<h2 id="Никогда_не_блокируется">Никогда не блокируется</h2>
<p>Очень интересное свойство цикла событий в JavaScript, что в отличие от множества других языков, поток выполнения никогда не блокируется. Обработка I/O обычно осуществляется с помощью событий и колбэк-функций, поэтому даже когда приложение ожидает запрос от <a href="/ru/docs/IndexedDB" title="/en-US/docs/IndexedDB">IndexedDB</a> или ответ от <a href="/ru/docs/Web/API/XMLHttpRequest" title="/en-US/docs/DOM/XMLHttpRequest">XHR</a>, оно может обрабатывать другие процессы, например пользовательский ввод.</p>
<p>Существуют хорошо известные исключения как <code style="font-style: normal;">alert</code> или синхронный XHR, но считается хорошей практикой избегать их использования.</p>
|