Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions 1-js/11-async/06-promisify/article.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Promisification

Promisification —— 一个长单词,用来描述一个简单的转换。它指将一个接受回调的函数转换为一个返回 promise 的函数。

准确一点说就是,我们创建了一个包装函数(wrapper-function)来做同样的事情,在内部调用原来的函数,但返回一个 promise。

在实际开发中经常需要这种转换,因为很多函数和库都是基于回调(callback-based)的。但是,使用 promise 更加方便。因此,将它们(函数和库)promisify 是很有意义的。

例如,章节 <info:callbacks> 里面写的 `loadScript(src, callback)`。

```js run
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

// 用法:
// loadScript('path/script.js', (err, script) => {...})
```

来对它进行 promisify 吧。新的函数 `loadScriptPromise(src)` 将会做同样的事情,但只接受 `src`(没有回调)并返回 promise。

```js
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err)
else resolve(script);
});
})
}

// 用法:
// loadScriptPromise('path/script.js').then(...)
```

现在, `loadScriptPromise` 非常适用于我们基于 promise(promise-based)的代码。

如我们所见,它将所有工作委派给原来的 `loadScript`,提供了自己的回调。此回调转换成了 promise 的 `resolve/reject`。

由于我们可能需要 promisify 很多函数,使用一个助手(helper)很有意义。

实际上很简单 —— 下面的 `promisify(f)` 接受一个要被 promisify 的函数,并返回一个包装函数。

这个包装函数做了跟上面代码一样的事情:返回 promise 并且把调用传递给原来的 `f`,在自定义的回调函数中跟踪结果:

```js
function promisify(f) {
return function (...args) { // 返回一个包装函数
return new Promise((resolve, reject) => {
function callback(err, result) { // 给 f 用的自定义回调
if (err) {
return reject(err);
} else {
resolve(result);
}
}

args.push(callback); // 在参数的最后附上我们自定义的回调函数

f.call(this, ...args); // 调用原来的函数
});
};
};

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
```

这里我们假设,原来的函数接受一个有两个参数 `(err, result)` 的回调。那是我们最经常遇到的(形式)。那么我们的自定义回调的格式确实正确,而且 `promisify` 在此案例中非常适用。

但是如果原来的 `f` 接受一个带更多参数的回调 `callback(err, res1, res2)`,该怎么办?

下面是 `promisify` 的修改版,它返回一个装有多个回调结果的数组:

```js
// 设定为 promisify(f, true) 来获取结果数组
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function *!*callback(err, ...results*/!*) { // 给 f 用的自定义回调
if (err) {
return reject(err);
} else {
// 如果 manyArgs 被指定值,则 resolve 所有回调结果
*!*resolve(manyArgs ? results : results[0]);*/!*
}
}

args.push(callback);

f.call(this, ...args);
});
};
};

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
```

在一些案例中,`err` 可能没有(在参数里):`callback(result)`,或者回调的格式有些奇怪,那么我们可以在不使用助手(helper)的情况下去手动实现 promisify。

也有一些提供更灵活一点的 promisification 函数的模块,例如 [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify)。在 Node.js 中,有一个内置的 promisify 函数 `util.promisify`。

```smart
Promisification 是一种很好的方法,特别是你使用 `async/await` 的时候(请看下一节),但不是回调函数的完全替代品。

请记住,一个 promise 可能只有一个结果,但是技术上,一个回调函数可能被多次调用。

因此 promisification 仅仅对调用一次回调的函数有用。以后的调用将会被忽略。
```
127 changes: 24 additions & 103 deletions 1-js/11-async/07-microtask-queue/article.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

# Microtasks 和事件循环
# 微任务(Microtasks

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

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

示例代码如下:

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

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

## Microtasks(微任务
## 微任务队列(Microtasks queue

异步任务需要适当的管理。为此,JavaScript 标准指定了一个内部队列 `PromiseJobs`,通常被称为 “microtask 队列”(v8 术语)。
异步任务需要适当的管理。为此,JavaScript 标准规定了一个内部队列 `PromiseJobs`,通常被称为 “微任务队列”(v8 术语)。
Comment thread
MartinsYong marked this conversation as resolved.

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

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

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

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

![](promiseQueue.png)

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

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

## 事件循环

浏览器内的 JavaScript 以及 Node.js 的执行流程都是基于**事件循环**的。

“事件循环”是引擎休眠并等待事件的过程。只有当事件发生时才会处理它们,然后重新进入休眠状态。

事件可能来自外部,例如用户操作,或者也可能来自于内部任务的结束信号。

例如下面的事件:
- `mousemove`,用户移动了他们的鼠标。
- `setTimeout` 处理程序将被调用。
- 一个外部的 `<script src="...">` 被加载完成,准备执行。
- 网络操作,例如 `fetch` 被完成。
- 等等。

事情发生了 —— 引擎处理它们 —— 并等待更多的事情发生(在休眠时消耗接近零 CPU)。

![](eventLoop.png)

如你所见,这里也有一个队列。所谓的 “macrotask(宏任务)队列”(v8 术语)。

当一个事件发生时,如果引擎正忙,它的处理程序就会进入这个队列排队等待执行。

例如,当引擎忙于处理网络 `fetch` 请求时,用户可能会移动他们的鼠标,这会触发 `mousemove`,或者相应的 `setTimeout` 等等,正如上图所示。

来自 macrotask 队列的事件基于“先进 —— 先处理”的原则被处理。当浏览器引擎完成 `fetch` 时,它会接着处理 `mousemove` 事件,然后是 `setTimeout` 处理程序,等等。

到目前为止,很简单,对吧?引擎正忙,所以其他任务需要排队。

现在重要的东西来了。

**Microtask 队列的优先级高于 macrotask 队列。**

换句话说,引擎会首先执行所有的 microtask,然后执行 macrotask。

例如,下面的代码:

```js run
setTimeout(() => alert("timeout"));

Promise.resolve()
.then(() => alert("promise"));

alert("code");
```

顺序是什么?

1. `code` 第一个出现,因为它是一个常规的同步调用。
2. `promise` 第二个出现,因为 `.then` 通过 microtask 队列被执行,并在当前代码之后运行。
3. `timeout` 最后出现,因为它来自于 macrotask 队列。

可能会发生这样的情况,在处理 macrotask 时,新的 promise 被创建。

或者,反过来说,一个 microtask 调度了一个 macrotask(例如 `setTimeout`)。

例如,这里的 `.then` 调度了一个 `setTimeout`:

```js run
Promise.resolve()
.then(() => {
setTimeout(() => alert("timeout"), 0);
})
.then(() => {
alert("promise");
});
```

当然,`promise` 会首先出现,因为 `setTimeout` 宏任务在一个低优先级的 macrotask 队列中进行等待。

作为一个合乎逻辑的结果,只有当 promise 给引擎一个“空闲时间”时才处理 macrotask。因此,如果我们有一系列不等待任何事情的 promise 处理程序,它们会被一个接一个地执行,`setTimeout`(或者用户操作处理程序)永远不能在它们之间运行。

## 未处理的 rejection

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

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

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

```js run
let promise = Promise.reject(new Error("Promise Failed!"));
*!*
promise.catch(err => alert('caught'));
*/!*

window.addEventListener('unhandledrejection', event => {
alert(event.reason); // Promise Failed!
});
// 不会运行:错误已被处理
window.addEventListener('unhandledrejection', event => alert(event.reason));
```

我们创建一个 rejected 的 `promise` 并且没有处理错误。所以我们有了一个 “未处理的 rejection”(也会在控制台打印出来)。

如果我们添加了 `.catch`,我们就不会见到这个“未处理的 rejection”,如下所示:
……但是如果我们忘记添加 `.catch`,那么微任务队列清空后,JavaScript 引擎会触发以下事件:

```js run
let promise = Promise.reject(new Error("Promise Failed!"));
*!*
promise.catch(err => alert('caught'));
*/!*

// 没有错误
// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
```

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

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

// 错误:Promise Failed!
// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
```

现在再次出现“未处理的 rejection”。为什么?因为 `unhandledrejection` 在 microtask 队列完成时才会被生成。而引擎会检查 promise,如果其中的任何一个出现 “rejected” 状态,`unhandledrejection` 事件就会被触发。

在这个例子中,`.catch` 当然被 `setTimeout` 触发器添加了,只是会在 `unhandledrejection` 出现之后被执行。
现在,如果你运行以上的代码,我们先会看到 `Promise Failed!` 的消息,然后才是 `caught`。

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

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

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

如果我们需要确保一段代码在 `.then/catch/finally` 之后被执行,最好将它添加到 `.then` 的链式调用中。

- 还有一个 “macrotask 队列”,用于保存各种事件,网络操作结果,`setTimeout` —— 调度的方法,等等。这些也被称为 “macrotasks(宏任务)”(v8 术语)。
## 总结

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

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

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

所以顺序是:常规代码,然后是 promise 处理程序,然后是其他的一切,比如事件等等
在大部分 JavaScript 引擎中(包括浏览器和 Node.js),微任务的概念与“事件循环”和“宏任务”紧密联系。由于这些概念跟 promises 没有直接关系,它们被涵盖在本教程另外的章节 <info:event-loop> 中
Binary file modified 1-js/11-async/07-microtask-queue/promiseQueue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 1-js/11-async/07-microtask-queue/promiseQueue@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.