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
160
161
162
163
|
---
title: 通用异步编程概念
slug: conflicting/Learn/JavaScript/Asynchronous/Introducing
tags:
- JavaScript
- Promises
- Threads
- 学习
- 异步
- 阻塞
translation_of: Learn/JavaScript/Asynchronous/Concepts
original_slug: Learn/JavaScript/Asynchronous/Concepts
---
<div>{{LearnSidebar}}{{NextMenu("Learn/JavaScript/Asynchronous/Introducing", "Learn/JavaScript/Asynchronous")}}</div>
<p>在本文中,我们将介绍与异步编程相关的一些重要概念,以及它们在浏览器和JavaScript里的体现。在学习本系列的其他文章之前,你应该先理解这些概念。</p>
<table class="learn-box standard-table">
<tbody>
<tr>
<th scope="row">预备条件:</th>
<td>拥有基本的计算机知识,对JavaScript原理有一定了解。</td>
</tr>
<tr>
<th scope="row">目标:</th>
<td>理解异步编程的基本概念,以及异步编程在浏览器和JavaScript里面的表现。</td>
</tr>
</tbody>
</table>
<h2 id="异步">异步?</h2>
<p>通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕.</p>
<p>Mac 用户有时会经历过这种旋转的彩虹光标(常称为沙滩球),操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。</p>
<p><img alt="multi-colored macos beachball busy spinner" src="https://mdn.mozillademos.org/files/16577/beachball.jpg" style="display: block; margin: 0 auto;"></p>
<p>这是令人沮丧的体验,没有充分利用计算机的计算能力 — 尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,同时计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是<strong>异步编程</strong>的出发点。你正在使用的编程环境(就web开发而言,编程环境就是web浏览器)负责为你提供异步运行此类任务的API。</p>
<h2 id="产生阻塞的代码">产生阻塞的代码</h2>
<p>异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做<strong>阻塞;</strong>这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。</p>
<p>我们来看一些<strong>阻塞</strong>的例子。</p>
<p>例子: <a href="https://github.com/mdn/learning-area/tree/master/javascript/asynchronous/introducing">simple-sync.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync.html">see it running live</a>), 在按钮上添加了一个事件监听器,当按钮被点击,它就开始运行一个非常耗时的任务(计算1千万个日期,并在console里显示最后一个日期),然后在DOM里面添加一个段落:</p>
<pre class="brush: js">const btn = document.querySelector('button');
btn.addEventListener('click', () => {
let myDate;
for(let i = 0; i < 10000000; i++) {
let date = new Date();
myDate = date
}
console.log(myDate);
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});</pre>
<p>运行这个例子的时候,打开JavaScript console,然后点击按钮 — 你会注意到,直到日期的运算结束,最后一个日期在console上显示出来,段落才会出现在网页上。代码按照源代码的顺序执行,只有前面的代码结束运行,后面的代码才会执行。</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: 这个例子不现实:在实际情况中一般不会发生,没有谁会计算1千万次日期,它仅仅提供一个非常直观的体验.</p>
</div>
<p>第二个例子, <a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/introducing/simple-sync-ui-blocking.html">simple-sync-ui-blocking.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-ui-blocking.html">see it live</a>), 我们模拟一个在现实的网页可能遇到的情况:因为渲染UI而阻塞用户的互动,这个例子有2个按钮:</p>
<ul>
<li>"Fill canvas" : 点击的时候用1百万个蓝色的圆填满整个{{htmlelement("canvas")}} .</li>
<li>"Click me for alert" :点击显示alert 消息.</li>
</ul>
<pre class="brush: js">function expensiveOperation() {
for(let i = 0; i < 1000000; i++) {
ctx.fillStyle = 'rgba(0,0,255, 0.2)';
ctx.beginPath();
ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
ctx.fill()
}
}
fillBtn.addEventListener('click', expensiveOperation);
alertBtn.addEventListener('click', () =>
alert('You clicked me!')
);</pre>
<p>如果你点击第一个按钮,然后快速点击第二个,会注意到alert消息并没有出现,只有等到圆圈都画完以后,才会出现:因为第一个操作没有完成之前阻塞了第二个操作的运行.</p>
<div class="blockIndicator note">
<p><strong>Note</strong>: 当然,这个例子也很丑陋,因为我们只是在模拟阻塞效果。但在现实中,这是一个很常见的问题。开发人员们一直在努力缓解它。</p>
</div>
<p>为什么是这样? 答案是:JavaScript一般来说是单线程的(<strong>single threaded</strong>)<strong>。</strong>接着我们来介绍<strong>线程</strong>的概念。</p>
<h2 id="线程">线程</h2>
<p>一个<strong>线程</strong>是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:</p>
<pre>Task A --> Task B --> Task C</pre>
<p>每个任务顺序执行,只有前面的结束了,后面的才能开始。</p>
<p>正如我们之前所说,现在的计算机大都有多个内核(core),因此可以同时执行多个任务。支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:</p>
<pre>Thread 1: Task A --> Task B
Thread 2: Task C --> Task D</pre>
<h3 id="JavaScript_是单线程的">JavaScript 是单线程的</h3>
<p>JavaScript 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(<strong>main thread</strong>)。我们上面的例子运行如下:</p>
<pre>Main thread: Render circles to canvas --> Display alert()</pre>
<p>经过一段时间,JavaScript获得了一些工具来帮助解决这种问题。通过 <a href="/en-US/docs/Web/API/Web_Workers_API">Web workers</a> 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)</p>
<pre>Main thread: Task A --> Task C
Worker thread: Expensive task B</pre>
<p>记住这些,请查看<a href="https://github.com/mdn/learning-area/blob/master/javascript/asynchronous/introducing/simple-sync-worker.html">simple-sync-worker.html</a> (<a href="https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-worker.html">see it running live</a>) , 再次打开浏览器的JavaScript 控制台。这个例子重写了前例:在一个单独的worker线程中计算一千万次日期,你再点击按钮,现在浏览器可以在日期计算完成之前显示段落,阻塞消失了。</p>
<h2 id="异步代码">异步代码</h2>
<p>web workers相当有用,但是他们确实也有局限。主要的一个问题是他们不能访问 {{Glossary("DOM")}} — 不能让一个worker直接更新UI。我们不能在worker里面渲染1百万个蓝色圆圈,它基本上只能做算数的苦活。</p>
<p>其次,虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。考虑下面的情况:</p>
<pre>Main thread: Task A --> Task B</pre>
<p>在这种情况下,比如说Task A 正在从服务器上获取一个图片之类的资源,Task B 准备在图片上加一个滤镜。如果开始运行Task A 后立即尝试运行Task B,你将会得到一个错误,因为图像还没有获取到。</p>
<pre>Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> | |</pre>
<p>在这种情况下,假设Task D 要同时使用 Task B 和Task C的结果,如果我们能保证这两个结果同时提供,程序可能正常运行,但是这不太可能。如果Task D 尝试在其中一个结果尚未可用的情况下就运行,程序就会抛出一个错误。</p>
<p>为了解决这些问题,浏览器允许我们异步运行某些操作。像<a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a> 这样的功能就允许让一些操作运行 (比如:从服务器上获取图片),然后等待直到结果返回,再运行其他的操作:</p>
<pre>Main thread: Task A Task B
Promise: |__async operation__|</pre>
<p>由于操作发生在其他地方,因此在处理异步操作的时候,主线程不会被阻塞。</p>
<p><span class="tlid-translation translation" lang="zh-CN"><span title="">我们将在下一篇文章中开始研究如何编写异步代码。</span> <span title="">非常令人兴奋,对吧?</span> <span title="">继续阅读!</span></span></p>
<h2 id="总结">总结</h2>
<p>围绕异步编程领域,现代软件设计正在加速旋转,就为了让程序在一个时间内做更多的事情。当你使用更新更强大的API时,你会发现在更多的情况下,使用异步编程是唯一的途径。以前写异步代码很困难,现在也需要你来适应,但是已经变容易了很多。在余下的部分,我们将进一步探讨异步代码的重要性,以及如何设计代码来防止前面已经提到过的问题。</p>
<h2 id="模块大纲">模块大纲</h2>
<ul>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Concepts">通用异步编程概念</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">介绍异步JavaScript</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals">合作异步JavaScript: Timeouts and intervals</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Promises">使用Promises优雅的异步编程</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Async_await">async and await:让异步编程更容易</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach">选择正确的方法</a></li>
</ul>
|