blob: f9fdf6920787f7a379ce946f71b249d3a0828e98 (
plain)
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
---
title: 并发模型与事件循环
slug: Web/JavaScript/EventLoop
tags:
- JavaScript
- 事件
- 事件处理
- 事件循环
- 事件管理
- 事件队列
- 进阶
translation_of: Web/JavaScript/EventLoop
---
<div>{{JsSidebar("Advanced")}}</div>
<p>JavaScript有一个基于<strong>事件循环</strong>的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。</p>
<h2 id="运行时概念">运行时概念</h2>
<p>接下来的内容解释了这个理论模型。现代JavaScript引擎实现并着重优化了以下描述的这些语义。</p>
<h3 id="可视化描述">可视化描述</h3>
<p style="text-align: center;"><img alt="Stack, heap, queue" src="https://mdn.mozillademos.org/files/17124/The_Javascript_Runtime_Environment_Example.svg" style="height: 271px; width: 295px;"></p>
<h3 id="栈">栈</h3>
<p>函数调用形成了一个由若干帧组成的栈。</p>
<pre class="brush: js">function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42</pre>
<p>当调用 <code>bar</code> 时,第一个帧被创建并压入栈中,帧中包含了 <code>bar</code> 的参数和局部变量。 当 <code>bar</code> 调用 <code>foo</code> 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 <code>foo</code> 的参数和局部变量。当 <code>foo</code> 执行完毕然后返回时,第二个帧就被弹出栈(剩下 <code>bar</code> 函数的调用帧 )。当 <code>bar</code> 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。</p>
<h3 id="堆">堆</h3>
<p>对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。</p>
<h3 id="队列">队列</h3>
<p>一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。</p>
<p>在 <a href="#事件循环">事件循环</a> 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。</p>
<p>函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。</p>
<h2 id="事件循环">事件循环</h2>
<p>之所以称之为 <strong>事件循环</strong>,是因为它经常按照类似如下的方式来被实现:</p>
<pre class="brush: js">while (queue.waitForMessage()) {
queue.processNextMessage();
}</pre>
<p><code>queue.waitForMessage()</code> 会同步地等待消息到达(如果当前没有任何消息等待被处理)。</p>
<h3 id="执行至完成">"执行至完成"</h3>
<p>每一个消息完整地执行后,其它消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。这与C语言不同,例如,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。</p>
<p>这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web应用程序就无法处理与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器一般会弹出一个“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。</p>
<h3 id="添加消息">添加消息</h3>
<p>在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。</p>
<p>函数 <code><a href="/zh-CN/docs/Web/API/Window/setTimeout">setTimeout</a></code> 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其它消息,<code>setTimeout</code> 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。</p>
<p>下面的例子演示了这个概念(<code>setTimeout</code> 并不会在计时器到期之后直接执行):</p>
<pre class="brush: js line-numbers language-js"><code class="language-js">const s = new Date().getSeconds();
setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}</code></pre>
<h3 id="零延迟">零延迟</h3>
<p>零延迟并不意味着回调会立即执行。以 0 为第二参数调用 <code>setTimeout</code> 并不表示在 0 毫秒后就立即调用回调函数。</p>
<p>其等待的时间取决于队列里待处理的消息数量。在下面的例子中,<code>"这是一条消息"</code> 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。</p>
<p>基本上,<code>setTimeout</code> 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。</p>
<pre class="brush: js">(function() {
console.log('这是开始');
setTimeout(function cb() {
console.log('这是来自第一个回调的消息');
});
console.log('这是一条消息');
setTimeout(function cb1() {
console.log('这是来自第二个回调的消息');
}, 0);
console.log('这是结束');
})();
// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"
</pre>
<h3 id="多个运行时互相通信">多个运行时互相通信</h3>
<p>一个 web worker 或者一个跨域的 <code>iframe</code> 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 <a href="/zh-CN/docs/Web/API/Window/postMessage"><code>postMessage</code></a> 方法进行通信。如果另一个运行时侦听 <code>message</code> 事件,则此方法会向该运行时添加消息。</p>
<h2 id="永不阻塞">永不阻塞</h2>
<p>JavaScript的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。 处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 <a href="/zh-CN/docs/Web/API/IndexedDB_API">IndexedDB</a> 查询返回或者一个 <a href="/zh-CN/docs/Web/API/XMLHttpRequest">XHR</a> 请求返回时,它仍然可以处理其它事情,比如用户输入。</p>
<p>由于历史原因有一些例外,如 <code>alert</code> 或者同步 XHR,但应该尽量避免使用它们。注意,<a href="https://stackoverflow.com/questions/2734025/is-javascript-guaranteed-to-be-single-threaded/2734311#2734311">例外的例外也是存在的</a>(但通常是实现错误而非其它原因)。</p>
<h2 id="标准规范">标准规范</h2>
<table class="standard-table">
<thead>
<tr>
<th scope="col">标准规范</th>
<th scope="col">状态</th>
<th scope="col">注释</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{SpecName('HTML WHATWG', 'webappapis.html#event-loops', 'Event loops')}}</td>
<td>{{Spec2('HTML WHATWG')}}</td>
<td></td>
</tr>
<tr>
<td><a href="https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/#what-is-the-event-loop">Node.js 事件循环</a></td>
<td>Living Standard</td>
<td></td>
</tr>
</tbody>
</table>
|