Skip to content

Commit e789877

Browse files
authored
Merge pull request javascript-tutorial#403 from MartinsYong/1-js/11-async-sync-patch
sync with upstream: 1-js/11-async, chapter 06(7d6d43) & 07(8365ea7)
2 parents 9fd548d + e4db74e commit e789877

4 files changed

Lines changed: 142 additions & 103 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Promisification
2+
3+
Promisification —— 一个长单词,用来描述一个简单的转换。它指将一个接受回调的函数转换为一个返回 promise 的函数。
4+
5+
准确一点说就是,我们创建了一个包装函数(wrapper-function)来做同样的事情,在内部调用原来的函数,但返回一个 promise。
6+
7+
在实际开发中经常需要这种转换,因为很多函数和库都是基于回调(callback-based)的。但是,使用 promise 更加方便。因此,将它们(函数和库)promisify 是很有意义的。
8+
9+
例如,章节 <info:callbacks> 里面写的 `loadScript(src, callback)`
10+
11+
```js run
12+
function loadScript(src, callback) {
13+
let script = document.createElement('script');
14+
script.src = src;
15+
16+
script.onload = () => callback(null, script);
17+
script.onerror = () => callback(new Error(`Script load error for ${src}`));
18+
19+
document.head.append(script);
20+
}
21+
22+
// 用法:
23+
// loadScript('path/script.js', (err, script) => {...})
24+
```
25+
26+
来对它进行 promisify 吧。新的函数 `loadScriptPromise(src)` 将会做同样的事情,但只接受 `src`(没有回调)并返回 promise。
27+
28+
```js
29+
let loadScriptPromise = function(src) {
30+
return new Promise((resolve, reject) => {
31+
loadScript(src, (err, script) => {
32+
if (err) reject(err)
33+
else resolve(script);
34+
});
35+
})
36+
}
37+
38+
// 用法:
39+
// loadScriptPromise('path/script.js').then(...)
40+
```
41+
42+
现在, `loadScriptPromise` 非常适用于我们基于 promise(promise-based)的代码。
43+
44+
如我们所见,它将所有工作委派给原来的 `loadScript`,提供了自己的回调。此回调转换成了 promise 的 `resolve/reject`
45+
46+
由于我们可能需要 promisify 很多函数,使用一个助手(helper)很有意义。
47+
48+
实际上很简单 —— 下面的 `promisify(f)` 接受一个要被 promisify 的函数,并返回一个包装函数。
49+
50+
这个包装函数做了跟上面代码一样的事情:返回 promise 并且把调用传递给原来的 `f`,在自定义的回调函数中跟踪结果:
51+
52+
```js
53+
function promisify(f) {
54+
return function (...args) { // 返回一个包装函数
55+
return new Promise((resolve, reject) => {
56+
function callback(err, result) { // 给 f 用的自定义回调
57+
if (err) {
58+
return reject(err);
59+
} else {
60+
resolve(result);
61+
}
62+
}
63+
64+
args.push(callback); // 在参数的最后附上我们自定义的回调函数
65+
66+
f.call(this, ...args); // 调用原来的函数
67+
});
68+
};
69+
};
70+
71+
// 用法:
72+
let loadScriptPromise = promisify(loadScript);
73+
loadScriptPromise(...).then(...);
74+
```
75+
76+
这里我们假设,原来的函数接受一个有两个参数 `(err, result)` 的回调。那是我们最经常遇到的(形式)。那么我们的自定义回调的格式确实正确,而且 `promisify` 在此案例中非常适用。
77+
78+
但是如果原来的 `f` 接受一个带更多参数的回调 `callback(err, res1, res2)`,该怎么办?
79+
80+
下面是 `promisify` 的修改版,它返回一个装有多个回调结果的数组:
81+
82+
```js
83+
// 设定为 promisify(f, true) 来获取结果数组
84+
function promisify(f, manyArgs = false) {
85+
return function (...args) {
86+
return new Promise((resolve, reject) => {
87+
function *!*callback(err, ...results*/!*) { //f 用的自定义回调
88+
if (err) {
89+
return reject(err);
90+
} else {
91+
// 如果 manyArgs 被指定值,则 resolve 所有回调结果
92+
*!*resolve(manyArgs ? results : results[0]);*/!*
93+
}
94+
}
95+
96+
args.push(callback);
97+
98+
f.call(this, ...args);
99+
});
100+
};
101+
};
102+
103+
// 用法:
104+
f = promisify(f, true);
105+
f(...).then(arrayOfResults => ..., err => ...)
106+
```
107+
108+
在一些案例中,`err` 可能没有(在参数里):`callback(result)`,或者回调的格式有些奇怪,那么我们可以在不使用助手(helper)的情况下去手动实现 promisify。
109+
110+
也有一些提供更灵活一点的 promisification 函数的模块,例如 [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify)。在 Node.js 中,有一个内置的 promisify 函数 `util.promisify`
111+
112+
```smart
113+
Promisification 是一种很好的方法,特别是你使用 `async/await` 的时候(请看下一节),但不是回调函数的完全替代品。
114+
115+
请记住,一个 promise 可能只有一个结果,但是技术上,一个回调函数可能被多次调用。
116+
117+
因此 promisification 仅仅对调用一次回调的函数有用。以后的调用将会被忽略。
118+
```
Lines changed: 24 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

2-
# Microtasks 和事件循环
2+
# 微任务(Microtasks
33

44
Promise 的处理程序(handlers)`.then``.catch``.finally` 都是异步的。
55

6-
即便一个 promise 立即被 resolve,`.then``.catch``.finally` **下面**的代码也会在这些处理程序之前被执行。
6+
即便一个 promise 立即被 resolve,`.then``.catch``.finally` *下面*的代码也会在这些处理程序之前被执行。
77

88
示例代码如下:
99

@@ -21,9 +21,9 @@ alert("code finished"); // 该警告框会首先弹出
2121

2222
为什么 `.then` 会在之后被触发?这是怎么回事?
2323

24-
## Microtasks(微任务
24+
## 微任务队列(Microtasks queue
2525

26-
异步任务需要适当的管理。为此,JavaScript 标准指定了一个内部队列 `PromiseJobs`,通常被称为 “microtask 队列”(v8 术语)。
26+
异步任务需要适当的管理。为此,JavaScript 标准规定了一个内部队列 `PromiseJobs`,通常被称为 “微任务队列”(v8 术语)。
2727

2828
[规范](https://tc39.github.io/ecma262/#sec-jobs-and-job-queues)中所述:
2929

@@ -32,7 +32,7 @@ alert("code finished"); // 该警告框会首先弹出
3232

3333
或者,简单地说,当一个 promise 准备就绪时,它的 `.then/catch/finally` 处理程序就被放入队列中。但是不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。
3434

35-
这就是示例中的 “code finished” 会首先出现的原因。
35+
这就是示例中的“code finished”会首先出现的原因。
3636

3737
![](promiseQueue.png)
3838

@@ -52,78 +52,6 @@ Promise.resolve()
5252

5353
现在代码就是按照预期执行的。
5454

55-
## 事件循环
56-
57-
浏览器内的 JavaScript 以及 Node.js 的执行流程都是基于**事件循环**的。
58-
59-
“事件循环”是引擎休眠并等待事件的过程。只有当事件发生时才会处理它们,然后重新进入休眠状态。
60-
61-
事件可能来自外部,例如用户操作,或者也可能来自于内部任务的结束信号。
62-
63-
例如下面的事件:
64-
- `mousemove`,用户移动了他们的鼠标。
65-
- `setTimeout` 处理程序将被调用。
66-
- 一个外部的 `<script src="...">` 被加载完成,准备执行。
67-
- 网络操作,例如 `fetch` 被完成。
68-
- 等等。
69-
70-
事情发生了 —— 引擎处理它们 —— 并等待更多的事情发生(在休眠时消耗接近零 CPU)。
71-
72-
![](eventLoop.png)
73-
74-
如你所见,这里也有一个队列。所谓的 “macrotask(宏任务)队列”(v8 术语)。
75-
76-
当一个事件发生时,如果引擎正忙,它的处理程序就会进入这个队列排队等待执行。
77-
78-
例如,当引擎忙于处理网络 `fetch` 请求时,用户可能会移动他们的鼠标,这会触发 `mousemove`,或者相应的 `setTimeout` 等等,正如上图所示。
79-
80-
来自 macrotask 队列的事件基于“先进 —— 先处理”的原则被处理。当浏览器引擎完成 `fetch` 时,它会接着处理 `mousemove` 事件,然后是 `setTimeout` 处理程序,等等。
81-
82-
到目前为止,很简单,对吧?引擎正忙,所以其他任务需要排队。
83-
84-
现在重要的东西来了。
85-
86-
**Microtask 队列的优先级高于 macrotask 队列。**
87-
88-
换句话说,引擎会首先执行所有的 microtask,然后执行 macrotask。
89-
90-
例如,下面的代码:
91-
92-
```js run
93-
setTimeout(() => alert("timeout"));
94-
95-
Promise.resolve()
96-
.then(() => alert("promise"));
97-
98-
alert("code");
99-
```
100-
101-
顺序是什么?
102-
103-
1. `code` 第一个出现,因为它是一个常规的同步调用。
104-
2. `promise` 第二个出现,因为 `.then` 通过 microtask 队列被执行,并在当前代码之后运行。
105-
3. `timeout` 最后出现,因为它来自于 macrotask 队列。
106-
107-
可能会发生这样的情况,在处理 macrotask 时,新的 promise 被创建。
108-
109-
或者,反过来说,一个 microtask 调度了一个 macrotask(例如 `setTimeout`)。
110-
111-
例如,这里的 `.then` 调度了一个 `setTimeout`
112-
113-
```js run
114-
Promise.resolve()
115-
.then(() => {
116-
setTimeout(() => alert("timeout"), 0);
117-
})
118-
.then(() => {
119-
alert("promise");
120-
});
121-
```
122-
123-
当然,`promise` 会首先出现,因为 `setTimeout` 宏任务在一个低优先级的 macrotask 队列中进行等待。
124-
125-
作为一个合乎逻辑的结果,只有当 promise 给引擎一个“空闲时间”时才处理 macrotask。因此,如果我们有一系列不等待任何事情的 promise 处理程序,它们会被一个接一个地执行,`setTimeout`(或者用户操作处理程序)永远不能在它们之间运行。
126-
12755
## 未处理的 rejection
12856

12957
还记得 <info:promise-error-handling> 一章中“未处理的 rejection”事件吗?
@@ -132,60 +60,53 @@ Promise.resolve()
13260

13361
**“未处理的 rejection”是指在 microtask 队列结束时未处理的 promise 错误。**
13462

135-
例如,考虑以下的代码
63+
正常来说,如果我们预期可能会发生错误,我们会添加 `.catch` 到 promise 链上去处理它
13664

13765
```js run
13866
let promise = Promise.reject(new Error("Promise Failed!"));
67+
*!*
68+
promise.catch(err => alert('caught'));
69+
*/!*
13970

140-
window.addEventListener('unhandledrejection', event => {
141-
alert(event.reason); // Promise Failed!
142-
});
71+
// 不会运行:错误已被处理
72+
window.addEventListener('unhandledrejection', event => alert(event.reason));
14373
```
14474

145-
我们创建一个 rejected 的 `promise` 并且没有处理错误。所以我们有了一个 “未处理的 rejection”(也会在控制台打印出来)。
146-
147-
如果我们添加了 `.catch`,我们就不会见到这个“未处理的 rejection”,如下所示:
75+
……但是如果我们忘记添加 `.catch`,那么微任务队列清空后,JavaScript 引擎会触发以下事件:
14876

14977
```js run
15078
let promise = Promise.reject(new Error("Promise Failed!"));
151-
*!*
152-
promise.catch(err => alert('caught'));
153-
*/!*
15479

155-
// 没有错误
80+
// Promise Failed!
15681
window.addEventListener('unhandledrejection', event => alert(event.reason));
15782
```
15883

159-
如下所示,现在我们假设会在 `setTimeout` 之后抓住这个错误
84+
如果我们迟点再处理这个错误会怎样?比如
16085

16186
```js run
16287
let promise = Promise.reject(new Error("Promise Failed!"));
16388
*!*
16489
setTimeout(() => promise.catch(err => alert('caught')));
16590
*/!*
16691

167-
// 错误:Promise Failed!
92+
// Error: Promise Failed!
16893
window.addEventListener('unhandledrejection', event => alert(event.reason));
16994
```
17095

171-
现在再次出现“未处理的 rejection”。为什么?因为 `unhandledrejection` 在 microtask 队列完成时才会被生成。而引擎会检查 promise,如果其中的任何一个出现 “rejected” 状态,`unhandledrejection` 事件就会被触发。
172-
173-
在这个例子中,`.catch` 当然被 `setTimeout` 触发器添加了,只是会在 `unhandledrejection` 出现之后被执行。
96+
现在,如果你运行以上的代码,我们先会看到 `Promise Failed!` 的消息,然后才是 `caught`
17497

175-
## 总结
98+
如果我们并不了解微任务队列,我们可能想知道:“为什么 `unhandledrejection` 的处理程序会运行?我们已经去捕捉(catch)这个错误了!”
17699

177-
- Promise 处理始终是异步的,因为所有 promise 操作都被放入内部的 “promise jobs” 队列执行,也被称为 “microtask 队列”(v8 术语)
100+
但是现在我们知道 `unhandledrejection` 在 microtask 队列完成时才会被生成:引擎会检查 promise,如果其中的任何一个出现“rejected”状态,`unhandledrejection` 事件就会被触发
178101

179-
**因此,`.then/catch/finally` 处理程序总是在当前代码完成后才被调用。**
102+
在这个例子中,被添加到 `setTimeout``.catch` 也会执行,只是会在 `unhandledrejection` 事件出现之后才执行,所以并没有改变什么(没有发挥作用)。
180103

181-
如果我们需要确保一段代码在 `.then/catch/finally` 之后被执行,最好将它添加到 `.then` 的链式调用中。
182-
183-
- 还有一个 “macrotask 队列”,用于保存各种事件,网络操作结果,`setTimeout` —— 调度的方法,等等。这些也被称为 “macrotasks(宏任务)”(v8 术语)。
104+
## 总结
184105

185-
引擎使用 macrotask 队列按出现顺序处理它们
106+
Promise 处理始终是异步的,因为所有 promise 操作都被放入内部的“promise jobs”队列执行,也被称为“微任务队列”(v8 术语)
186107

187-
**Macrotasks 在当前代码执行完成并且 microtask 队列为空时执行**
108+
**因此,`.then/catch/finally` 处理程序总是在当前代码完成后才被调用**
188109

189-
换句话说,它们的优先级较低
110+
如果我们需要确保一段代码在 `.then/catch/finally` 之后被执行,最好将它添加到 `.then` 的链式调用中
190111

191-
所以顺序是:常规代码,然后是 promise 处理程序,然后是其他的一切,比如事件等等
112+
在大部分 JavaScript 引擎中(包括浏览器和 Node.js),微任务的概念与“事件循环”和“宏任务”紧密联系。由于这些概念跟 promises 没有直接关系,它们被涵盖在本教程另外的章节 <info:event-loop>
739 Bytes
Loading
1.82 KB
Loading

0 commit comments

Comments
 (0)