我有一个异步函数,它由我的代码中的某个地方的setInterval运行。此函数定期更新一些缓存。
我还有一个不同的同步函数需要检索值 - 最好是从缓存中检索,但如果它是缓存未命中,则从数据源开始 (我意识到以同步的方式进行IO操作是不明智的,但我们假设在这种情况下需要这样做。)
我的问题是我希望同步函数能够等待来自异步函数的值,但是不可能在非await
函数中使用async
关键字:
function syncFunc(key) {
if (!(key in cache)) {
await updateCacheForKey([key]);
}
}
async function updateCacheForKey(keys) {
// updates cache for given keys
...
}
现在,通过将updateCacheForKey
内的逻辑提取到一个新的同步函数中,并从两个现有函数中调用这个新函数,可以很容易地避免这种情况。
我的问题是为什么首先绝对防止这个用例?我唯一的猜测是它与“白痴”有关,因为在大多数情况下,从同步函数等待异步函数是错误的。但我认为它有时会有其有效的用例我错了吗?
(我认为使用Task.Wait
也可以在C#中实现,但我可能会在这里混淆一些事情。)
答案 0 :(得分:20)
我的问题是我希望同步函数能够等待来自异步函数的值......
他们不能,因为:
JavaScript基于由线程处理的“作业队列”,其中作业具有运行到完成语义,
JavaScript实际上没有异步函数(真的 - 坚持下去......)
作业队列(事件循环)在概念上非常简单:当需要完成某些事情(脚本的初始执行,事件处理程序回调等)时,该工作将被放入作业队列中。为该作业队列提供服务的线程获取下一个待处理作业,将其运行完成,然后返回下一个作业。 (当然,它比那更复杂,但这足以满足我们的目的。)因此,当一个函数被调用时,它被调用作为处理作业的一部分,并且作业总是处理完成在下一份工作开始之前。
运行完成意味着如果作业调用了一个函数,那么该函数必须在作业完成之前返回。当线程运行以执行其他操作时,作业不会在中间暂停。这使得代码显着更容易正确编写和推理,而不是在其他事情发生时,作业可能会被暂停。 (同样,它比这更复杂,但这又足以满足我们的目的。)
到目前为止一切顺利。这有什么关于没有真正具有异步功能的东西?!
虽然我们讨论“同步”与“异步”函数,甚至我们可以将async
关键字应用于函数,但函数调用在JavaScript中始终是 synchronous 。异步函数并不存在。我们有同步函数可以设置环境稍后调用的回调(通过排队作业)。
我们假设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;
});
}
它要求浏览器开始获取数据的过程,并在其中注册回调(通过then
),以便浏览器在数据返回时调用,然后退出,从then
返回承诺。尚未提取数据,但updateCacheForKey
已完成。它已经回来了。它同步完成了它的工作。
稍后,当获取完成时,浏览器将作业排队以调用该承诺回调;当从队列中获取该作业时,将调用回调,并且其返回值用于解析返回的promise then
。
我的问题是为什么要首先完全阻止这个用例?
让我们看看它会是什么样子:
线程选择一个工作,该工作涉及调用syncFunc
,调用updateCacheForKey
。 updateCacheForKey
要求浏览器获取资源并返回其承诺。通过这个非异步await
的魔力,我们同步等待该承诺得到解决,从而阻止了这项工作。
在某些时候,浏览器的网络代码完成检索资源并对作业进行排队,以调用我们在updateCacheForKey
中注册的承诺回调。
没有任何事情再次发生。 : - )
...因为作业具有运行到完成语义,并且在完成前一个作业之前,不允许该线程接收下一个作业。不允许该线程暂停中间调用syncFunc
的作业,以便它可以处理解决该承诺的作业。
这似乎是随意的,但同样,它的原因在于它使得编写正确的代码和理解代码正在做的事情变得非常容易。
但它确实意味着“同步”功能不能等待“异步”功能完成。
上面有一些很多的细节等等。如果你想深入了解它的细节,你可以深入研究规范。包装很多条款和温暖的衣服,你会有一段时间。 : - )
答案 1 :(得分:1)
现在,可以通过将updateCacheForKey内部的逻辑提取到新的同步函数中,并从两个现有函数中调用此新函数来轻松地避免这种情况。
T.J. Crowder完美地解释了JavaScript中异步函数的语义。但我认为以上段落值得更多讨论。根据{{1}}所做的事情,可能无法将其逻辑提取到同步函数中,因为在JavaScript中,某些事情只能异步完成。例如,无法执行网络请求并同步等待其响应。如果updateCacheForKey
依赖于服务器响应,则不能将其转换为同步功能。
甚至在异步函数和承诺出现之前,情况就是如此:例如,updateCacheForKey
获得回调,并在响应准备就绪时调用它。无法同步获取响应。 Promises只是回调的抽象层,异步函数只是Promise的抽象层。
现在,这个本来可以完成的。在某些环境中它是 :
XMLHttpRequest
,readFileSync
等),直到该操作完成为止。writeFileSync
和朋友(alert
,confirm
),这些朋友会阻塞直到用户关闭模式对话框。这表明JavaScript语言的设计者可以选择prompt
,XMLHttpRequest
等的同步版本。为什么没有?
[W]为什么首先要完全避免这种使用情况?
这是设计决定。
例如, fetch
阻止用户与页面的其余部分进行交互,因为JavaScript是单线程的,并且阻塞了唯一的执行线程,直到alert
调用完成。因此,无法执行事件处理程序,这意味着无法进行交互。如果有alert
函数,它将阻止用户执行任何操作,直到网络请求完成为止,这可能要花费几分钟甚至几小时甚至几天。
这显然与我们称为“网络”的交互式环境的性质背道而驰。回顾起来,syncFetch
是一个错误,除非在极少数情况下,否则不应该使用它。
唯一的选择是允许在JavaScript中进行多线程处理,这是很难编写正确的程序的。您在围绕异步函数时遇到麻烦了吗?尝试信号量!
答案 2 :(得分:1)
可以在异步函数中添加一个很好的旧 .then() 并且它会起作用。
应该考虑而不是这样做,而是将当前的常规函数更改为异步函数,并一直向上调用调用堆栈,直到不需要返回的承诺,即从异步函数返回的值没有任何工作要做。在这种情况下,它实际上可以从同步调用。
答案 3 :(得分:0)
您可以如下所示在非异步方法中使用异步函数调用
注意:(async()=>等待updateCacheForKey([key]));
function syncFunc(key) {
if (!(key in cache)) {
(async () => await updateCacheForKey([key]));
}
}
async function updateCacheForKey(keys) {
// updates cache for given keys
...
}