请不要嵌套承诺
多个异步操作
更好的方法
道路async
await
结论
const fs = require('fs');
// Callback-based Asynchronous Code
fs.readFile('file.txt', (err, text) => {
if (err) throw err;
console.log(text)
});
// ES6 Promises
fs.promises.readFile('file.txt')
.then(console.log)
.catch(console.error);
多年来,回调模式一直被 JavaScript 视为异步代码的事实上的设计模式。ES6 Promises 终于在 2015 年问世,其目标是简化异步操作。它最终消除了可怕的回调地狱,即嵌套回调函数看似无限的倒退。得益于 ES6 Promises,异步 JavaScript 突然变得可以说更简洁、更易读……真的吗?🤔
多个异步操作
当同时执行多个异步操作时,可以利用它Promise.all
来有效地实现这一目标,而不会导致
太多了事件循环的问题。
在下面基于 的示例中Promise
,一个 数组Promises
将被传入该Promise.all
方法。JavaScript 引擎在底层巧妙地运行了这三个并发 readFile
操作。一旦它们全部解析完毕,Promise#then
链中后续操作的回调才能最终执行。否则,如果至少有一个操作失败,则Error
该操作的对象将被传入最近的Promise#catch
。
const fs = require('fs');
const FILES = [ 'file1.txt', 'file2.txt', 'file3.txt' ];
// Callback-based
function callback(err, text) {
if (err) throw err;
console.log(text);
}
for (const file of FILES)
fs.readFile(file, callback);
// `Promise`-based
const filePromises = FILES.map(file => fs.promises.readFile(file));
Promise.all(filePromises)
.then(texts => console.log(...texts))
.catch(console.error);
只有当多个异步操作需要按照特定顺序依次执行时,Promise 的问题才会开始显现。这时,回调地狱就会再次出现在基于回调和基于 Promise 的异步链中。
const fs = require('fs');
const fsp = fs.promises;
// The Traditional Callback Hell
fs.readFile('file1.txt', (err, text1) => {
if (err) throw err;
console.log(text1);
fs.readFile('file2.txt', (err, text2) => {
if (err) throw err;
console.log(text2);
fs.readFile('file3.txt', (err, text3) => {
if (err) throw err;
console.log(text3);
// ...
});
});
});
// The Modern "Promise" Hell
fsp.readFile('file1.txt')
.then(text1 => {
console.log(text1);
fsp.readFile('file2.txt')
.then(text2 => {
console.log(text2);
fsp.readFile('file3.txt')
.then(text3 => {
console.log(text3));
// ...
})
.catch(console.error);
})
.catch(console.error);
})
.catch(console.error);
更好的方法
解决嵌套 Promise 的问题,可以这样操作:回调函数的返回值总是会被包装在一个 resolve 中,Promise
该 resolve 稍后会被转发给Promise#then
链中的下一个回调函数(如果它Promise
本身还不是)。这样,下一个Promise#then
回调函数就可以使用前一个回调函数的返回值,以此类推……
换句话说,返回值总是被包装在 resolve 中Promise
并转发给链中的下一个Promise#then
。后者可以通过相应的回调函数检索转发的返回值。对于抛出的值(理想情况下是Error
对象),也是如此,它们会作为 rejection 转发给链中的Promise
下一个。Promise#catch
// Wrap the value `42` in
// a resolved promise
Promise.resolve(42)
// Retrieve the wrapped return value
.then(prev => {
console.log(prev);
// Forward the string 'Ping!'
// to the next `Promise#then`
// in the chain
return 'Ping!';
})
// Retrieve the string 'Ping!' from
// the previously resolved promise
.then(prev => {
console.log(`Inside \`Promise#then\`: ${prev}`);
// Throw a random error
throw new Error('Pong!');
})
// Catch the random error
.catch(console.error);
// Output:
// 42
// 'Inside `Promise#then`: Ping!'
// Error: Pong!
有了这些知识,上面的“Promise Hell”示例现在可以重构为更“线性”的流程,而无需不必要的缩进和嵌套。
const fsp = require('fs').promises;
fsp.readFile('file1.txt')
.then(text1 => {
console.log(text1);
return fsp.readFile('file2.txt');
})
.then(text2 => {
console.log(text2);
return fsp.readFile('file3.txt');
})
.then(text3 => {
console.log(text3);
// ...
})
.catch(console.error);
事实上,这种“线性”的 Promise 流程正是Fetch API基本示例所倡导的模式。请考虑以下与GitHub REST API v3进行基本交互的示例:
// Main endpoint for the GitHub REST API
const API_ENDPOINT = 'https://api.github.com/';
fetch(API_ENDPOINT, { method: 'GET' })
// `Response#json` returns a `Promise`
// containing the eventual result of the
// parsed JSON from the server response.
// Once the JSON has been parsed,
// the promise chain will forward the
// result to the next `Promise#then`.
// If the JSON has been malformed in any
// way, then an `Error` object will be
// constructed and forwarded to the next
// `Promise#catch` in the chain.
.then(res => res.json())
.then(console.log)
.catch(console.error);
道路async
await
ES2017 中备受喜爱的async
/await
特性,如今可以解决异步操作顺序敏感的问题。它将繁琐的回调函数、无休止的Promise#then
链式调用以及不必要的程序逻辑嵌套隐藏在直观的抽象层之后。从技术上讲,它为异步操作提供了一种同步流程的错觉,从而使其更容易理解。
const fsp = require('fs').promises;
async function readFiles() {
try {
console.log(await fsp.readFile('file1.txt'));
console.log(await fsp.readFile('file2.txt'));
console.log(await fsp.readFile('file3.txt'));
} catch (err) {
console.error(err);
}
}
尽管如此,此功能仍然容易被滥用。虽然异步函数需要对 Promise 进行重大反思,但旧习难改。旧的 Promise 思维方式(通过嵌套回调)很容易与 ES2017 异步函数的新流程和概念产生有害的混淆。考虑以下示例,我称之为“弗兰肯斯坦地狱”,因为它将回调模式、“线性”Promise 流程和异步函数混杂在一起,令人困惑:
const fs = require('fs');
// Needless to say... this is **very** bad news!
// It doesn't even need to be many indentations
// deep to be a code smell.
fs.readFile('file1.txt', async (err, text1) => {
console.log(text1);
const text2 = await (fs.promises.readFile('file2.txt')
.then(console.log)
.catch(console.error));
});
更糟糕的是,上面的例子甚至可能导致内存泄漏。这方面的讨论超出了本文的范围,但James Snell在2019 年 Node+JS Interactive 大会上的演讲“Broken Promises”中详细解释了这些问题。
结论
ES6 Promises 和 ES2017 异步函数——虽然本身可读性高且功能强大——但仍需付出一些努力才能保持其优雅性。为了避免回调地狱及其令人讨厌的轮回,精心规划和设计异步流程至关重要。
嵌套的 Promise 是一种代码异味,可能表明整个代码库中存在一些 Promise 的不当使用。由于回调的返回值始终会被转发到Promise#then
链中下一个回调的回调,因此,我们可以通过重构来改进它们,从而充分利用回调的返回值和异步函数(如果可行)。
请不要嵌套 Promise。即使是 Promise 也可能会引入可怕的回调地狱。
文章来源:https://dev.to/somedood/please-don-t-nest-promises-3o1o