关于 async/await 和 Promises 的陷阱

2025-06-09

关于 async/await 和 Promises 的陷阱

JavaScript 一直以来都具有异步的特性。尽管大多数 Web API 都是同步的,但由于 JavaScript 中函数是“一等公民”,情况最终也发生了变化。现在,基本上每个新的 JavaScript API 都被设计为异步的。(即使是几十年前的 Cookie API 也可能会进行异步改造。)

当我们必须序列化这些异步任务时,问题就出现了,这意味着在回调结束时执行异步方法等等。实际上,我们必须这样做:

$.get('/api/movies/' + movieCode, function(movieData) {
  $.get('/api/directors/' + movieData.director, function(directorData) {
    $.get('/api/studios/' + directorData.studio, function(studioData) {
      $.get('/api/locations/' + studioData.hq, function(locationData) {
        // do something with locationData
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

是的,这就是毁灭金字塔。(这只是一个简单的例子:当你必须并行执行异步任务时事情就变得疯狂了。)

然后Promise,s 和 ES2015 一起来了。嗯,承诺会把我们的代码变成这样:

doSomething()
  .then(data => doStuff(data))
  .then(result => doOtherStuff(result))
  .then(outcome => showOutcome(outcome));
Enter fullscreen mode Exit fullscreen mode

不错,易读,语义清晰。但在实践中,我们得到的往往是类似这样的结果:

doSomething().then(data => {
  doStuff(data).then(result => {
    doOtherStuff(data, result).then(outcome => {
      showOutcome(outcome, result, data);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

金字塔又来了!发生什么事了?!

这基本上发生在一个任务不仅依赖于前一个任务的结果,还依赖于更早任务的结果时。当然,你可以这样做:

let _data;
let _result;
doSomething().then(data => {
  _data = data;
  return doStuff(data);
}).then(result => {
  _result = result;
  return doOtherStuff(_data, result);
}).then(outcome => {
  showOutcome(outcome, _result, _data);
});
Enter fullscreen mode Exit fullscreen mode

我甚至不想指出这有多么尴尬和刺耳。我们在赋值之前就声明了我们需要的变量,如果你像我一样,const每当变量的值预计不会改变时,都会有“必须使用”的强迫症,那么你会觉得这些let感觉就像刺痛了你的瞳孔。

但 ES2016 来了,它带来了async“/”的await甜蜜!它承诺 (...) 能把我们的乱七八糟的代码变成这种同步代码:

const data = await doSomething();
const result = await doStuff(data);
const outcome = await doOtherStuff(data, result);
await showOutcome(outcome, result, data);
Enter fullscreen mode Exit fullscreen mode

好的!

但是……事情一如既往地没那么简单。让我们拭目以待。

任何承诺都不应落空

尤其如此,因为 Promise 的拒绝并不会抛出错误。虽然近年来浏览器和 Node 的智能程度有所提升,但过去,未处理拒绝的 Promise 总是默默地失败……而且非常致命。更不用说调试起来的麻烦了。

await现在,当执行被拒绝的承诺时会发生什么?

它抛出了。

你可能会想,解决这个问题轻而易举。我们try...catch很久以前就遇到过:

try {
  const data = await doSomething();
} catch (e) {
  console.error('Haha, gotcha!', e.message);
}
Enter fullscreen mode Exit fullscreen mode

……现在,我必须问一下。你们 JavaScript 开发者中,有多少人觉得用 es写起来很舒服try...catch?JavaScript 一直以来都是一门非常宽容的语言,大多数时候我们只需要检查一个值是否为null之类的。再加上 JavaScript 在处理 时性能不佳try...catch,你肯定会遇到尴尬的情况。

(虽然最近情况有所改变。虽然之前 V8 没有优化内部代码try...catch,但随着 V8 6.0 和 TurboFan 随 Chrome 60 和 Node 8.3 一起推出,情况已经不再如此,我想其他浏览器供应商也会很快赶上来。所以我们最终会遇到原生s的常见性能问题Promise。)

范围困境

好吧,我们不得不把原本好好的await一行代码改成 5 行try...catch。这已经够糟糕的了,但不幸的是,这还不是全部。让我们再检查一下代码:

try {
  const data = await doSomething();
} catch (e) { ... }

// Doing something with data...
Enter fullscreen mode Exit fullscreen mode

好吧,我们又倒霉了:我们没法用,data因为它超出了我们的作用域!事实上,它的作用域只在try块内部!我们该怎么解决这个问题呢?

... 而解决方案同样很糟糕:

let data;
try {
  data = await doSomething();
} catch (e) { ... }

// Doing something with data...
Enter fullscreen mode Exit fullscreen mode

再次使用 ... 预先声明变量let几乎是被迫再次使用的var实际上它不会那么糟糕,因为使用async/await你的函数可能具有平坦作用域,而你的变量无论如何都会具有闭包作用域。但是 linters 会告诉你你的代码很烂,你的强迫症会让你睡不着觉,咖啡会变酸,小猫会难过等等。

我们取得的唯一进展是我们可以在区块之前使用let 因此try...catch事情就不会那么令人不快了:

let data;
try {
  data = await doSomething();
} catch (e) { ... }

let result;
try {
  result = await doStuff(data);
} catch (e) { ... }
Enter fullscreen mode Exit fullscreen mode

宝可梦解决方案

如果你关心小猫咪的幸福,你就得做点什么。以下是一些常见、简单、“我有事可做”的方法:

try {
  const data = await doSomething();
  const result = await doStuff(data);
  const outcome = await doOtherStuff(data, result);
  await showOutcome(outcome, result, data);
} catch(e) {
  console.error('Something went wrong, deal with it 🕶¸', e.message);
}
Enter fullscreen mode Exit fullscreen mode

我跟你说,你还是睡不着。没错,你“得把它们全都抓起来”,但不是那样的。你已经被无数次地教导过,这很糟糕,你应该感到难过,尤其是在 JavaScript 中,你不能依靠多个catch代码块来区分异常类型,而是必须用代码块instanceof甚至message属性来检查它们。

按规矩办事

你拉钩保证你永远不会做那种事,并且会按照规定做事。可能的情况是:

try {
  const data = await doSomething();
  const result = apparentlyInnocentFunction(data);
  return result;
} catch(e) {
  console.error('Error when doingSomething, check your data', e.message);
}
Enter fullscreen mode Exit fullscreen mode

没错,我们正在捕获被拒绝的 Promise。但之后会发生什么呢?没什么,我们只是调用了一个(表面上)无害的函数来转换数据。

……我们确定吗?这个函数真的那么好用吗?

问题在于 a仍然try...catcha 。它不仅会捕获ed 的 Promise,还会捕获所有抛出的错误,无论我们是否预料到。为了正确处理,我们应该使用来包装ed的Promise try...catchawaittry...catchawait

丑陋、冗长、痛苦,但却是必要的。

我们在使用Promises 时就已经见过这种情况了,所以这应该不是什么新鲜事。简而言之,不要这样做:

doSomething.then(data => {
  const result = apparentlyInnocentFunction(data);
  return result;
}).catch(error => {
  console.error('Error when doingSomething, check your data', e.message);
});
Enter fullscreen mode Exit fullscreen mode

这样做:

doSomething.then(data => {
  const result = apparentlyInnocentFunction(data);
  return result;
}, error => { // <= catching with the second argument of `then`!
  console.error('Error when doingSomething, check your data', e.message);
});
Enter fullscreen mode Exit fullscreen mode

一个好的妥协?

那么,我们该如何处理这个烂摊子呢?一个好的解决方案是完全摆脱try...catch块,利用Promises ,记住它们catch本身有一个方法可以Promise再次返回 a 。我们这样做:

const data = await doSomething()
    .catch(e => console.error('Error when doingSomething', e.message));
if (!data) { /* Bail out somehow */ }
Enter fullscreen mode Exit fullscreen mode

就我个人而言,我对此百感交集。这样是不是更好?我们是不是在混合使用不同的技巧?我想这主要取决于我们处理什么,所以就到这里吧。

请记住:

  • await不只是解析Promises,而且解析任何具有方法的对象——thenable 试试then这个:)await {then() {console.log('Foo!')}}
  • 不仅如此,您还可以使用await 任何对象,甚至是字符串或null

这意味着thencatch可能未定义,或者与你想象的不同。(另外请记住 是 的语法.catch(f).then(null, f),因此后者就是定义 thenable 所需的全部内容。)

隐藏的并行性

如何一次性解决多个并行(或者更好,并发)的 Promise?我们一直依赖Promise.all

Promise.all([ doSomething(), doSomethingElse() ]).then(...);

// or in terms of await:
await Promise.all([ doSomething(), doSomethingElse() ]);
Enter fullscreen mode Exit fullscreen mode

但 Cory House 最近给出了这个建议:

因此,即使没有它,也可以解决并发承诺

const a = doSomething();
const b = doSomethingElse();
// Just like await Promise.all([a, b])
await a, await b;
Enter fullscreen mode Exit fullscreen mode

这里的技巧在于,promise在被执行之前就已经被初始化了await。直接等待函数调用而不是执行aandb会导致序列化执行。

我的建议是:小心这些潜在的并发问题;不要自作聪明地试图利用它。使用Promise.all更易于阅读。

不仅仅是糖

你可能听说过, async/await就像 JavaScript 的许多其他新功能一样,只是经典 ES5 JavaScript 中已经可以实现的功能的语法糖。这基本上是正确的,但就像许多其他情况(类、箭头函数等)一样,它还有更多含义。

正如 Mathias Bynens最近指出的那样,JS 引擎必须做大量工作才能从Promise链中获得不错的堆栈跟踪,因此使用async/await无疑是更好的选择。

问题在于,我们不能随心所欲地使用它。我们仍然需要支持 IE 或 Node 6.x 等不支持新语法的旧版浏览器。但我们也不要忽视像 UC 和三星浏览器这样同样不支持新语法的浏览器!最终,我们必须对所有内容进行转译,而且这项工作还会持续一段时间。

更新(2018 年 3 月):三星互联网和 UC 浏览器现在都支持async/ await,但要注意旧版本。

结论

我不知道你的情况,但我对转译async函数的体验……目前为止不太理想。Chrome 似乎在处理 Sourcemap 方面存在一些 bug,或者可能它们定义不明确,但无论如何。

我该用async/await吗?当然用,但我觉得由于前面提到的所有问题,我并没有像我希望的那样频繁地使用它。它绝对是未来的趋势,但对于未来的趋势,我们必须持保留态度。

async您对/有何经验await

鏂囩珷鏉ユ簮锛�https://dev.to/maxart2501/gotchas-about-asyncawait-and-promises-9di
PREV
让我们开发一个二维码生成器,第一部分:基本概念
NEXT
我的 2019 年 DEV 之旅 - 查看你的数据!想知道你的数据吗?快来试试吧!