我有一个异步函数,它由我的代码中某处的 setInterval 运行。此函数定期更新一些缓存。
我还有一个不同的同步函数需要检索值 - 最好从缓存中检索值,但如果它是缓存未命中,则从数据来源(我意识到以同步方式进行 IO 操作是不明智的,但让我们假设在这种情况下这是必需的)。
我的问题是我希望同步函数能够等待来自异步函数的值,但是不能在非 async
函数中使用 await
关键字:
function syncFunc(key) {
if (!(key in cache)) {
await updateCacheForKey([key]);
}
}
async function updateCacheForKey(keys) {
// updates cache for given keys
...
}
现在,可以通过将 updateCacheForKey
中的逻辑提取到一个新的同步函数中,并从两个现有函数中调用这个新函数来轻松绕过这个问题。
我的问题是为什么首先要绝对阻止这个用例?我唯一的猜测是它与“防白痴”有关,因为在大多数情况下,等待来自同步函数的异步函数是错误的。但是我认为它有时有其有效的用例是错误的吗?
(我认为这在 C# 中也可以通过使用 Task.Wait
来实现,尽管我在这里可能会混淆)。
await
,则函数根据定义 是异步的。为什么不直接将 async
添加到 syncFunc
?或者您是说您希望 syncFunc
在等待时阻止线程?
syncFunc
阻止
我的问题是我希望同步函数能够等待来自异步函数的值...
他们不能,因为:
JavaScript 基于由线程处理的“作业队列”工作,其中作业具有运行到完成的语义,而 JavaScript 并没有真正的异步函数——即使是异步函数,在幕后也是返回承诺的同步函数(详情如下)
作业队列(事件循环)在概念上非常简单:当需要完成某些事情时(脚本的初始执行、事件处理程序回调等),该工作被放入作业队列中。为该作业队列提供服务的线程拾取下一个待处理的作业,将其运行到完成,然后返回下一个。 (当然,它比这更复杂,但这对于我们的目的来说已经足够了。)因此,当一个函数被调用时,它会作为作业处理的一部分被调用,并且作业总是在下一个作业运行之前被处理完成。
运行完成意味着如果作业调用了一个函数,则该函数必须在作业完成之前返回。当线程跑去做其他事情时,作业不会在中间暂停。这使得代码更容易正确编写和推理,而不是在发生其他事情时作业可能会在中间暂停。 (同样它比这更复杂,但这对于我们这里的目的来说已经足够了。)
到目前为止,一切都很好。没有真正的异步函数是怎么回事?!
尽管我们讨论了“同步”与“异步”函数,甚至还有一个可以应用于函数的 async
关键字,但在 JavaScript 中函数调用始终是同步。 async
函数是一个同步返回一个函数的逻辑实现或拒绝稍后的函数,将环境稍后调用的回调排队。
让我们假设 updateCacheForKey
看起来像这样:
async function updateCacheForKey(key) {
const value = await fetch(/*...*/);
cache[key] = value;
return value;
}
在幕后真正做的是(非常粗略,不是字面意思)是这样的:
function updateCacheForKey(key) {
return fetch(/*...*/).then(result => {
const value = result;
cache[key] = value;
return value;
});
}
(我在我最近的书JavaScript: The New Toys的第 9 章中对此进行了更详细的介绍。)
它要求浏览器开始获取数据的过程,并在其中注册一个回调(通过 then
)供浏览器在数据返回时调用,然后退出,返回then
的承诺。数据尚未提取,但 updateCacheForKey
已完成。它已经回来了。它同步完成它的工作。
稍后,当 fetch 完成时,浏览器会排队一个作业来调用该 promise 回调;当从队列中提取该作业时,将调用回调,并使用其返回值来解决返回的承诺 then
。
我的问题是为什么首先要绝对阻止这个用例?
让我们看看会是什么样子:
线程选择一个作业,该作业涉及调用syncFunc,后者调用updateCacheForKey。 updateCacheForKey 要求浏览器获取资源并返回其承诺。通过这种非异步等待的魔力,我们同步等待该承诺被解决,从而阻止了工作。在某个时刻,浏览器的网络代码完成了对资源的检索,并将作业排队以调用我们在 updateCacheForKey 中注册的承诺回调。什么都没有发生,再一次。 :-)
...因为作业具有运行到完成的语义,并且在完成前一个作业之前不允许线程拿起下一个作业。不允许线程暂停在中间调用 syncFunc
的作业,以便它可以继续处理将解决承诺的作业。
这似乎是任意的,但同样,这样做的原因是它使得编写正确的代码和推理代码正在做什么变得非常容易。
但这确实意味着“同步”函数不能等待“异步”函数完成。
上面有很多细节等等。如果您想深入了解它的本质,可以深入研究规范。带上大量的食物和保暖的衣服,你会有一些时间。 :-)
作业和作业队列
执行上下文
领域和代理
您可以通过 Immediately Invoked Function Expression (IIFE) 从 non-async 函数中调用 async 函数:
(async () => await updateCacheForKey([key]))();
并应用于您的示例:
function syncFunc(key) {
if (!(key in cache)) {
(async () => await updateCacheForKey([key]))();
}
}
async function updateCacheForKey(keys) {
// updates cache for given keys
...
}
await
放入其中,然后在没有 await
的情况下调用外部异步函数。因此,外部函数将一直运行,直到到达 await
,然后将控制权返回给 syncFunc
。然后 await
之后的所有内容将在 syncFunc
完成后运行。您可以通过直接从 runSync
调用 updateCacheForKey
来实现类似的效果。
updateCacheForKey()
实际上执行任何异步操作,则不会立即调用它。在这种情况下,立即发生的所有事情就是创建了一个 Promise(稍后执行)。 然而如果 updateCacheForKey() 是真正同步的,没有返回 Promise 或等待,那么它会立即调用它。在这种情况下,这是一个非常酷的 hack,可以绕过只有 async
函数才能使用 await
的限制。 (顺便说一句,被调用的函数有时可以同步,有时可以异步!)
现在,可以通过将 updateCacheForKey 中的逻辑提取到一个新的同步函数中,并从两个现有函数中调用这个新函数来轻松绕过这个问题。
T.J. Crowder 完美地解释了 JavaScript 中异步函数的语义。但在我看来,以上段落值得更多讨论。根据 updateCacheForKey
的作用,可能无法将其逻辑提取到同步函数中,因为在 JavaScript 中,有些事情只能异步完成。例如,无法同步执行网络请求并等待其响应。如果 updateCacheForKey
依赖于服务器响应,则无法将其转换为同步函数。
甚至在异步函数和 Promise 出现之前也是如此:例如,XMLHttpRequest
获取回调并在响应准备好时调用它。无法同步获得响应。 Promise 只是回调的抽象层,异步函数只是 Promise 的抽象层。
现在,这可能会有所不同。它在某些环境中:
在 PHP 中,几乎所有内容都是同步的。你用 curl 发送一个请求,你的脚本会阻塞,直到它得到响应。
Node.js 有其文件系统调用的同步版本(readFileSync、writeFileSync 等),这些调用会阻塞直到操作完成。
即使是普通的旧浏览器 JavaScript 也有警报和朋友(确认、提示),它们会阻止直到用户关闭模式对话框。
这表明 JavaScript 语言的设计者可以选择 XMLHttpRequest
、fetch
等的同步版本。为什么他们不这样做?
[W]为什么首先要绝对阻止这种用例?
这是一个设计决定。
例如,alert
会阻止用户与页面的其余部分进行交互,因为 JavaScript 是单线程的,并且唯一的执行线程被阻止,直到 alert
调用完成。因此无法执行事件处理程序,这意味着无法交互。如果有 syncFetch
函数,它将阻止用户执行任何操作,直到网络请求完成,这可能需要几分钟、甚至几小时或几天。
这显然违背了我们称之为“网络”的交互环境的本质。 alert
回想起来是一个错误,除非在极少数情况下,否则不应使用它。
唯一的选择是在 JavaScript 中允许多线程,这是众所周知的难以编写正确程序的方法。您是否难以理解异步函数?试试信号量!
可以将一个好的旧 .then() 添加到 async 函数中,它会起作用。
应该考虑而不是这样做,将您当前的常规函数更改为异步函数,并一直向上调用堆栈直到不需要返回的承诺,即没有任何工作可以处理从异步函数返回的值。在这种情况下,它实际上可以从同步调用。
这显示了一个函数如何同时是同步和异步的,以及立即调用函数表达式习语如何仅在通过被调用函数的路径执行同步操作时才是立即的。
function test() {
console.log('Test before');
(async () => await print(0.3))();
console.log('Test between');
(async () => await print(0.7))();
console.log('Test after');
}
async function print(v) {
if(v<0.5)await sleep(5000);
else console.log('No sleep')
console.log(`Printing ${v}`);
}
function sleep(ms : number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
test();
(基于对另一个答案的评论中的 Ayyappa's code。)
console.log 如下所示:
16:53:00.804 Test before
16:53:00.804 Test between
16:53:00.804 No sleep
16:53:00.805 Printing 0.7
16:53:00.805 Test after
16:53:05.805 Printing 0.3
如果您将 0.7 更改为 0.4,一切都将异步运行:
17:05:14.185 Test before
17:05:14.186 Test between
17:05:14.186 Test after
17:05:19.186 Printing 0.3
17:05:19.187 Printing 0.4
如果你将这两个数字都更改为超过 0.5,一切都会同步运行,并且根本不会创建任何 Promise:
17:06:56.504 Test before
17:06:56.504 No sleep
17:06:56.505 Printing 0.6
17:06:56.505 Test between
17:06:56.505 No sleep
17:06:56.505 Printing 0.7
17:06:56.505 Test after
不过,这确实暗示了对原始问题的回答。你可以有这样的功能(免责声明:未经测试的nodeJS代码):
const cache = {}
async getData(key, forceSync){
if(cache.hasOwnProperty(key))return cache[key] //Runs sync
if(forceSync){ //Runs sync
const value = fs.readFileSync(`${key}.txt`)
cache[key] = value
return value
}
//If we reach here, the code will run async
const value = await fsPromises.readFile(`${key}.txt`)
cache[key] = value
return value
}
不定期副业成功案例分享
updateCacheForKey
的结果,它需要在它返回的 Promise 上使用then
。 (async
函数返回承诺。)