关于 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
});
});
});
});
是的,这就是毁灭金字塔。(这只是一个简单的例子:当你必须并行执行异步任务时,事情就变得疯狂了。)
然后Promise
,s 和 ES2015 一起来了。嗯,承诺会把我们的代码变成这样:
doSomething()
.then(data => doStuff(data))
.then(result => doOtherStuff(result))
.then(outcome => showOutcome(outcome));
不错,易读,语义清晰。但在实践中,我们得到的往往是类似这样的结果:
doSomething().then(data => {
doStuff(data).then(result => {
doOtherStuff(data, result).then(outcome => {
showOutcome(outcome, result, data);
});
});
});
金字塔又来了!发生什么事了?!
这基本上发生在一个任务不仅依赖于前一个任务的结果,还依赖于更早任务的结果时。当然,你可以这样做:
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);
});
我甚至不想指出这有多么尴尬和刺耳。我们在赋值之前就声明了我们需要的变量,如果你像我一样,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);
好的!
但是……事情一如既往地没那么简单。让我们拭目以待。
任何承诺都不应落空
尤其如此,因为 Promise 的拒绝并不会抛出错误。虽然近年来浏览器和 Node 的智能程度有所提升,但过去,未处理拒绝的 Promise 总是默默地失败……而且非常致命。更不用说调试起来的麻烦了。
await
现在,当执行被拒绝的承诺时会发生什么?
它抛出了。
你可能会想,解决这个问题轻而易举。我们try...catch
很久以前就遇到过:
try {
const data = await doSomething();
} catch (e) {
console.error('Haha, gotcha!', e.message);
}
……现在,我必须问一下。你们 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...
好吧,我们又倒霉了:我们没法用,data
因为它超出了我们的作用域!事实上,它的作用域只在try
块内部!我们该怎么解决这个问题呢?
... 而解决方案同样很糟糕:
let data;
try {
data = await doSomething();
} catch (e) { ... }
// Doing something with data...
再次使用 ... 预先声明变量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) { ... }
宝可梦解决方案
如果你关心小猫咪的幸福,你就得做点什么。以下是一些常见、简单、“我有事可做”的方法:
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);
}
我跟你说,你还是睡不着。没错,你“得把它们全都抓起来”,但不是那样的。你已经被无数次地教导过,这很糟糕,你应该感到难过,尤其是在 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);
}
没错,我们正在捕获被拒绝的 Promise。但之后会发生什么呢?没什么,我们只是调用了一个(表面上)无害的函数来转换数据。
……我们确定吗?这个函数真的那么好用吗?
问题在于 a仍然try...catch
是a 。它不仅会捕获ed 的 Promise,还会捕获所有抛出的错误,无论我们是否预料到。为了正确处理,我们应该使用来包装ed的Promise 。try...catch
await
try...catch
await
丑陋、冗长、痛苦,但却是必要的。
我们在使用Promise
s 时就已经见过这种情况了,所以这应该不是什么新鲜事。简而言之,不要这样做:
doSomething.then(data => {
const result = apparentlyInnocentFunction(data);
return result;
}).catch(error => {
console.error('Error when doingSomething, check your data', e.message);
});
这样做:
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);
});
一个好的妥协?
那么,我们该如何处理这个烂摊子呢?一个好的解决方案是完全摆脱try...catch
块,利用Promise
s ,记住它们catch
本身有一个方法可以Promise
再次返回 a 。我们这样做:
const data = await doSomething()
.catch(e => console.error('Error when doingSomething', e.message));
if (!data) { /* Bail out somehow */ }
就我个人而言,我对此百感交集。这样是不是更好?我们是不是在混合使用不同的技巧?我想这主要取决于我们处理什么,所以就到这里吧。
请记住:
await
不只是解析Promise
s,而且解析任何具有方法的对象——thenable (试试then
这个:);await {then() {console.log('Foo!')}}
- 不仅如此,您还可以使用
await
任何对象,甚至是字符串或null
。
这意味着then
或catch
可能未定义,或者与你想象的不同。(另外请记住 是 的语法.catch(f)
糖.then(null, f)
,因此后者就是定义 thenable 所需的全部内容。)
隐藏的并行性
如何一次性解决多个并行(或者更好,并发)的 Promise?我们一直依赖Promise.all
:
Promise.all([ doSomething(), doSomethingElse() ]).then(...);
// or in terms of await:
await Promise.all([ doSomething(), doSomethingElse() ]);
但 Cory House 最近给出了这个建议:
因此,即使没有它,也可以解决并发承诺:
const a = doSomething();
const b = doSomethingElse();
// Just like await Promise.all([a, b])
await a, await b;
这里的技巧在于,promise在被执行之前就已经被初始化了await
。直接等待函数调用而不是执行a
andb
会导致序列化执行。
我的建议是:小心这些潜在的并发问题;不要自作聪明地试图利用它。使用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
?