请不要嵌套承诺多个异步操作更好的方法异步/等待方法结论

2025-06-07

请不要嵌套承诺

多个异步操作

更好的方法

道路asyncawait

结论

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);
Enter fullscreen mode Exit fullscreen mode

多年来,回调模式一直被 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);
Enter fullscreen mode Exit fullscreen mode

只有当多个异步操作需要按照特定顺序依次执行时,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);
Enter fullscreen mode Exit fullscreen mode

更好的方法

解决嵌套 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!
Enter fullscreen mode Exit fullscreen mode

有了这些知识,上面的“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);
Enter fullscreen mode Exit fullscreen mode

事实上,这种“线性”的 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);
Enter fullscreen mode Exit fullscreen mode

道路asyncawait

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

尽管如此,此功能仍然容易被滥用。虽然异步函数需要对 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));
});
Enter fullscreen mode Exit fullscreen mode

更糟糕的是,上面的例子甚至可能导致内存泄漏。这方面的讨论超出了本文的范围,但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
PREV
请不要写令人困惑的条件
NEXT
JavaScript 并发:避免顺序陷阱 简介 范围和限制 空闲执行 Promise.all Promise.allSettled 单线程语言的注意事项 Promise 和工作线程 结论