TL;DR
不,不可能在 JavaScript 中将异步函数包装在同步函数中并传播结果。请参阅此excellent blog post on sync vs. async functions in various languages。要点是,由于语言运行方式的性质,JavaScript 是(许多语言中的一种)异步函数具有传染性。
异步内置函数是JS中的救星
JavaScript 在一个线程上运行。更具体地说,与特定网页相关的所有 JavaScript 都在同一个线程上运行,以保证在任何特定时刻只有 一行 行 JS 将运行。这让我们尼安德特人的 web 程序员免去了编写诸如互斥锁和原子操作之类的同步代码的责任,以免多个线程同时写入同一内存并导致数据损坏甚至崩溃。
但是,我们只有一个线程来操作网页上的视觉元素和运行各种业务逻辑,如加密/解密和数据管理,这有点糟糕。这一切都可能变得有点慢并损害用户体验。但是异步函数如何解决这个问题?取这个函数:
function syncGenRSAKey() {
// fancy math stuff...
return generatedKey;
}
让我们让它异步(基于承诺):
function asyncGenRSAKey() {
return new Promise((resolve, reject) => {
resolve(syncGenRSAKey());
});
}
希望您的直觉不会告诉您基于 Promise 的函数在这里更快。所发生的一切都是这样的:
- 部分代码调用
asyncGenRSAKey()
- 浏览器运行
Promise构造函数
- Promise 构造函数立即/同步调用传递给它的
(resolve, reject) => { ... } 回调函数
- 浏览器运行
syncGenRSAKey()函数
- 承诺同步实现
我们的代码仍然是完全同步的。我们一无所获。请记住,我们的 JavaScript 一次永远只能运行 一行。只要我们的底层密钥生成代码 (syncGenRSAKey()) 是用 JavaScript 编写的,无论从哪里调用它,它总是会占用主线程上的时间。这意味着它将阻止浏览器跳转到其他 JavaScript,即事件处理程序。浏览器还在主线程上渲染页面,因此当genRSAKey() 运行时,它将冻结页面上的几乎所有内容(一些 CSS 动画被特别渲染)。用户可以悬停按钮,按钮背景和鼠标光标都不会更新。
现在,请参阅我回答的这一部分的副标题。关键字是内置。内置函数,如 crypto.subtle 下提供的函数,是用浏览器实现者选择的任何语言编写的:C++、Rust 等。这些函数不是由 JavaScript 引擎运行,它们是它的一部分。它们可以生成尽可能多的操作系统线程,以在您的计算机在给定时刻可以腾出的尽可能多(或尽可能少)的 CPU 内核上运行。这意味着密钥生成代码可以并且通常将与您的一堆 JavaScript 代码和页面呈现选项完全并行运行,然后当密钥准备好并且任何当前正在运行的 JavaScript 完成运行时,浏览器只会回调您的 JavaScript ,触发要解决的承诺(或在生成密钥时发生错误时拒绝),然后可以启动链接到生成密钥的任何承诺中的代码。
现在,这对于SHA-256 校验和真的有必要吗?不。事实上,我自己还有一个 GitHub PR 我一直在推迟,因为我厌倦了 promisifying 一切(包括一些非常复杂的 Angular 组件),因为我计算 一个 f**king hash 当用户打开一个模态框时。苏珊娜,这是给你的。
以下是两个优秀的视频,任何阅读此 StackOverflow 帖子的人都应该花时间观看。除非您对 JavaScript 的同步/异步特性有足够的了解,能够准确地描绘出您的代码将如何运行,否则您并不真正了解 JavaScript,并且最终会遇到无法理解的错误.
The Node.js Event Loop: Not So Single Threaded
Jake Archibald: In The Loop - JSConf.Asia
JavaScript 中async/await 的说明
async 和await 关键字是纯语法糖。它们不能让你做任何你以前使用老式的 Promise 链不能做的事情,就像 Promise 不能让你做任何你用好的 ole 嵌套回调函数做不到的事情一样。 async/await 只需让您的代码清洁 10 倍。最后,与使用嵌套回调相比,Promise 实际上会产生少量的运行时开销,因为 Promise 具有各种状态以方便将它们很好地链接起来并且是堆分配的; async/await,我听说,可以通过让 JS 引擎更容易地查看异步代码的整体上下文以及使用变量的位置等来撤消这个小小的退步,并进行优化。
以下是正确使用async/await 的一些常见示例。为了清楚返回类型,它们是用 TypeScript 编写的,但如果你去掉 : Whatevers,它就会变成 JavaScript。
在基于 Promise 的 API 中封装一个同步函数
这实际上很少需要,但有时您需要您的代码适合第三方代码所需的接口,例如库。
function withoutAsyncAwait(): Promise<number> {
// Note that the reject callback provided to us by the Promise
// constructor is rarely useful because the promise will
// automatically be rejected if our callback throws an error,
// e.g., if the Math.random() throws an error.
return new Promise((resolve, reject) => resolve(Math.random()));
// Could be (ignore the reject callback):
// return new Promise(resolve => resolve(Math.random()));
}
async function withAsyncAwait(): Promise<number> {
// If any synchronous code inside an async function throws an
// error, a promise will still be returned by the async function,
// but it will be rejected (by far the only desirable behavior).
// The same is true if an await'ed promise rejects.
return Math.random();
}
如果您包装传统的基于回调的方法,您不能(以及为什么要)避免使用 Promise 构造函数
async 作为 Promise 发挥作用。
function timeout(milliseconds: number): Promise<void> {
return new Promise(resolve => window.setTimeout(resolve, milliseconds));
}
条件异步步骤
有时您希望在一堆同步代码之前有条件地执行异步操作。前
async/await 这意味着您必须复制同步代码或将其全部包装在承诺链中
如果条件不成立,最初的承诺将是空操作。
function doStuffWithoutAsyncAwait1(needToMakeAsyncRequest: boolean): Promise<void> {
// Might be a no-op promise if we don't need to make a request before sync code
const promise = needToMakeAsyncRequest ? makeAsyncRequest() : Promise.resolve();
return promise.then(() => {
// tons of code omitted here, imagine like 30 lines...
});
}
function doStuffWithoutAsyncAwait2(needToMakeAsyncRequest: boolean): Promise<void> {
// Or we can just write the sync code twice, wrapping it in a promise in the branch
// where we make an async request first. This sucks because our 30 lines of sync
// code is written twice AND one of the times it is nested/indented inside of both
// an if-statement and a .then() call
if (needToMakeAsyncRequest) {
return makeAsyncRequest().then(() => {
// tons of code omitted here, imagine like 30 lines...
});
}
// tons of code omitted here, imagine like 30 lines...
}
async function cmereAsyncAwaitYouSexyBoiYou(needToMakeAsyncRequest: boolean): Promise<void> {
if (needToMakeAsyncRequest) {
// Brings tears to my eyes ?
await makeAsyncRequest();
}
// tons of code omitted here, imagine like 30 lines...
}
结合 async/await 和现有的 promise 机制
async/await 不是灵丹妙药。它使编写异步步骤的序列变得非常干净
但有时我们不只是想要一个序列:我们希望同时运行多个异步步骤。
async function takes12SecondsTotal(): Promise<[string, string]> {
const result1 = await takes7Seconds();
const result2 = await takes5Seconds(); // will not get here till 1st result is done
return [result1, result2];
}
async function takes7SecondsTotal(): Promise<[string, string]> {
// Both inner functions start doing stuff immediately and we just wait for them
// both to finish
const [result1, result2] = await Promise.all([
takes7Seconds(),
takes5Seconds()
]);
return [result1, result2];
}
function nottttttActuallyyyyyTheSammeeeeIKnowIKnowScrewErrorHandling(): Promise<[string, string]> {
// We are almost there! However, we just introduced a potential sh!tstorm by reducing down our
// code and getting rid of async/await: we now have the assumption that both the takes7Seconds()
// and takes5Seconds() calls DO return promises... but they might have synchronous code and the
// beginning of them that could throw an error because the author screwed up and then they will
// blow up SYNCHRONOUSLY in our face and this function will also blow up SYNCHRONOUSLY and it
// will continue up the call stack until it hits a try-catch or it reaches all the way out and
// the JS engine stops it and logs it in the dev tools
return Promise.all([
takes7Seconds(),
takes5Seconds()
]);
// Let me illustrate:
function takes5Seconds(): Promise<string> {
const now = new Date; // Trivia: you don't need constructor parenthesis if no parameters
if (now.getDay() === 6 && now.getHours() === 21) { // 9pm on a Saturday
// Synchronous error
throw Error("I ain't workin' right now, ok?")
}
// Returns a promise, whose rejection will be handled by the promise chain, so an
// "asynchronous" error (but this function could also throw a synchronous error, you
// never know)
return doSomeWork();
}
}
function thisIsFunctionallyTheSame(): Promise<[string, string]> {
try {
return Promise.all([
takes7Seconds(),
takes5Seconds()
]);
} catch (err) {
// catch any synchronous error and gift-wrap it in a promise to protect whoever calls
// us from a synchronous error explosion
return Promise.reject(err);
}
}
async function justBeSmartAndUseAsync(): Promise<[string, string]> {
// Even though we don't use await at all, async functions act as a stalwart line of defense,
// stopping any synchronous errors thrown from continuing up the callstack, implicitly
// catching them and making sure we return a promise NO MATTER WHAT (implicitly does what
// I did above but the browser probably does it better since async functions are part of the
// language spec and lots of work has been and will be put into optimizing them)
return Promise.all([
takes7Seconds(),
takes5Seconds()
]);
}
我们甚至可能希望同时运行多个异步步骤序列。
async function youCouldBeForgivenForDoingThis(): Promise<void> {
// Please edit this answer if I'm wrong, but last time I checked, an await keyword holds up
// the entire expression it's part of--in our case, that means the entire Promise.all(...)
// expression. The doSomethingUnrelated() will not even start running until writeCode()
// finishes
await Promise.all([
pushCodeToGitHub(await writeCode()),
doSomethingUnrelated()
]);
}
async function armedWithEsotericJSKnowledge(): Promise<void> {
// Also please note I just await the Promise.all to discard the array of undefined's and
// return void from our async function
await Promise.all([
writeCode().then(code => pushCodeToGitHub(code)),
doSomethingUnrelated()
]);
}
永远不要害怕将 promise 存储在变量中,或者将 async 箭头函数混合到传统的
.then()promise 链是获取最智能代码所必需的。
在异步函数中返回的深奥的废话*t
如果您使用 TypeScript 或通常熟悉 JS 承诺,您可能已经知道
在 .then() 回调中,您可以返回类型 T 或 Promise<T> 和承诺
内部机制确保只有一个普通的T 被传递给下一个.then()
链上。 T 可以是 number 或任何其他类型。 async 函数做同样的事情
事物。错误处理没有那么简单。
function getNumber(): number {
return 420;
}
async function getNumberAsync(): Promise<number> {
return getNumber(); // auto-wrap it in a promise cuz we're an async function
}
async function idkJavaScriptButIWantToMakeSureIGetThatNumber(): Promise<number> {
return await getNumberAsync(); // this IS fine, really
}
async function iKNOWJavaScript(): Promise<number> {
return getNumberAsync(); // this will NOT return Promise<Promise<number>> because async unwraps it
}
function iLikeToBlowUpRandomly(): Promise<number> {
if (Math.random() > 0.5) {
// This is not an async function so this throw clause will NOT get wrapped in a rejected promise
// and returned pleasantly to the caller
throw new Error("boom");
}
return getNumberAsync();
}
async function iHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
try {
return iLikeToBlowUpRandomly();
} catch (err) {
// This will always catch the "boom" explosions, BUT, if iLikeToBlowUpRandomly() returns a
// rejected promise, it will sneakily slip through our try-catch because try-catches only
// catch THROWN errors, and whoever called us will get a bad promise even though we
// promised (haha) we would only ever return fulfilled promises containing numbers
return -1;
}
}
async function iActuallyHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
try {
// Bam! The normally extraneous await here brings this promise into our pseudo-synchronous
// async/await code so if it was rejected, it will also trigger our catch branch just like
// a synchronous error would
return await iLikeToBlowUpRandomly();
} catch (err) {
return 3522047650; // call me if you have job offers ? but I'm kinda busy rn and spent way too much time on this
}
}