为什么此异步/等待功能返回承诺?

时间:2019-09-25 17:40:01

标签: javascript asynchronous promise async-await

当我使用async / await运算符时,我不确定为什么以下函数会返回promise?

getBase64 = async (url) => {
  const response = await axios.get(url, {
  responseType: 'arraybuffer'
  })

  const buffer = new Buffer.from(response.data,'binary').toString('base64')
  return ('data:image/jpeg;base64,' + buffer)
}

我知道我可以简单地添加.then(data => console.log(data)),但是我想将原始数据分配给变量,例如:

const base64Img = getBase64()

...这是不可能的,因为出于某种原因,返回类型是一个承诺。

3 个答案:

答案 0 :(得分:2)

异步功能只是一个承诺。如果您希望返回该值,则还需要等待它。

const bas64Img = await getBase64()

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

答案 1 :(得分:0)

关于async函数始终返回承诺的简短简短的答案是,因为ECMAScript规范定义了async函数的行为mandates so

这是我将尝试解释的选择依据,但我已经可以告诉您它与语言规范的其他相关部分(例如execution contextsjobs and job queues)有关。< / p>

之所以制定这些规范,部分原因是出于历史原因-ECMAScript起源于Web浏览器中的JavaScript,以及它们允许网页脚本编写的方式。由于在多种实现方式实施一段时间之后,该语言已实现标准化,因此ECMAScript的定义是追溯性的:

  

ECMAScript的第五版(作为ECMA-262第5版发布)对语言规范的事实上的解释进行了编纂,这些解释在浏览器实现中已变得很普遍[...]

(摘自"Introduction"的第7段)

在任何情况下,至关重要的是,ECMAScript不允许并行脚本执行-例如,JavaScript函数调用绝不会被任何其他函数调用句号打断。不在同一个realm中(在Web浏览器中,一个领域与一个单独的呈现的Web页面相关联)。

如果我们设想一个JavaScript运行时的任务是调用一个async函数,那么我们可以更深入地研究“为什么”,并查看运行时安排该返回值的实际情况。取决于异步操作。因此,以您的getBase64函数为例。现在,我们这样做显然违反了语言规范,但这只是为了做出解释。

getBase64的主体内部,我们假设axios.get(...)是一个潜在的冗长操作,因为它在网络上获取资源。如果我们假设不是,则getBase64不必为async,不能使用await,并且其行为类似于常规函数。因此,出于这个答案的考虑,我们确实假定它涉及一个冗长的操作,并且在此冗长的操作完成之前,我们没有response的值。 axios.get可能正在通过网络获取资源,但是操作的本质并不重要,重要的是,该操作可能比完成整个脚本的执行花费更长的时间。

那么,我们可以继续执行getBase64的主体并在axios.get(...)完成之前返回吗?为了论证,我们可以执行一些部分,它们不依赖取决于axios.get(...)返回的承诺解决。这就是所谓的自动代码并行化。 ECMAScript没有定义它,甚至可能不允许它。很少有语言能做到。但是正如我所提到的,从技术上讲,可以在函数主体中评估不依赖于已解决的承诺的那些语句,但是无论如何,getBase64的返回值间接取决于response ,因此我们至少需要冗长的操作才能返回。那很重要。

因此,如果我们还不能返回,则必须等待axios.get(...)返回的诺言解决。我的意思是,从response返回值之前,我们需要一种getBase64

在这里变得很有趣。脚本运行时始终具有实际编写脚本的环境。此环境的一部分可能是浏览器窗口(window),网页(document)或其他全部东西。作为兼容ECMAScript运行时的显着示例,Node.js也具有一个环境。然后想到一个问题–如果getBase64等待axios.get(...)解决时,环境需要调度事件怎么办?可能有传入的网络请求(Node.js)或用户单击链接(Web浏览器)。您还附加了一个事件监听器(让您无法响应的事件没有多大意义,对吗?)。现在,您是在getBase64调用等待response的同时调用并开始执行事件侦听器,还是等待getBase64和脚本的其余部分的执行(想一想)?呼叫链)来完成,冒着使用户等待潜在的“较长”时间(例如,网络“缓慢”)而带来的不良用户体验的风险?

如果开始执行事件处理程序,则不可避免地最终会并行出现两个脚本执行上下文-事件处理程序执行和getBase64调用等待axios.get(...)解析。实际上,即使getBase64仍在进行中,事件处理程序仍在执行,即使代理仅使用一个执行线程,而前者优先于后者。它仍然是并行脚本执行。这不是小事-脚本的行为,特别是正在执行的语句的顺序,现在取决于事件的计时!您将不得不处理潜在的竞争条件,并大体上说明状态同步问题。

如果不允许允许并行脚本执行,则必须对其进行序列化。意思是完成一个脚本的执行,例如完成getBase64调用和最后调用该脚本的其余脚本,然后才开始执行另一个脚本,例如执行事件处理程序。

这就是ECMAScript设计人员追求的目标。他们决定禁止并行脚本执行,以换取简单性,因为这样做可以简化开发人员,使他们不必处理上面简要提到的整个并行编程问题。这样限制了运行时开发人员,因为执行同一脚本的不同兼容 runtimes 将以相同的顺序(而不取决于事件时间)评估脚本中存在的语句。

有些人可能将此称为语言的过分规范化,但我相信我已经对此做了某种解释。在任何情况下,JavaScript在标准化之前都具有更小的野心,并且在没有人讲CPU的情况下,通过避免源于并行脚本执行的结果以及更简单的Web浏览器体系结构,可以相对轻松地实现脚本。 “核心”是优先事项。 ECMAScript使用严格指定的模型对该行为进行了标准化。

当脚本执行花费太长时间才能完成时,这当然仍然给我们带来“冻结”页面的问题,因为执行事件处理程序和任何影响脚本所处环境的东西都必须延迟到当前执行完成为止。而且,如果您曾经尝试计算页面脚本中足够复杂的内容,那么您就会明白我的意思。没有两个脚本并行执行。间隔,计时器,promise和其他操作的回调的执行被排队并在它们自己的执行上下文中完成,脚本执行也是如此。这些作业和其他作业(由不同类型的API发起的网络请求和操作的实际进展)使用ECMAScript作业队列。最重要的是,HTML指定了一个名为event loops的东西,它在内部也使用作业队列。这些规范严格定义了事件的处理方式(在不同的API触发回调时),否则,脚本环境仍可以在希望短暂执行的脚本执行之间保持响应。

实际上,您可能已经从"AsyncFunctionStart" definition of the specification中看出,async函数的执行在定义明确的点被拆分为多个作业。按照规范,没有并行脚本执行,但是该函数仍然有效地“等待”每个await表达式。

因此,总的来说,是的,从技术上讲,我们可以让async函数始终返回您通过return关键字告诉它们返回的值,但是您将在JavaScript开发中进行极大的简化返回另一种“简单性”,允许async函数的中断(通常交错执行上下文),以便掩盖回调/承诺处理并能够直接使用返回值,但需要注意状态变化更加紧密地对待运行时的特性,并保持警惕。 ECMAScript设计师为您做出了选择。

为什么 some async函数不能返回“实际”值?

即使不允许并行脚本执行,从技术上讲,任何事情都不会阻止不使用async的{​​{1}}函数返回您想要的值。这样的功能将能够执行到完成而无需等待任何承诺。然后,基本上它就像一个普通函数一样运作,await是“仅以名称”。但是您会违反ECMAScript规范,该规范要求async函数始终返回承诺。再次由ECMAScript规范的设计者做出决定。

我不知道他们做出决定的原因,但我敢说这是因为这样一来,就不必研究async函数的实现(“是否有{{1} }关键字?”),以便知道如何使用它-可以使用相同类型的代码(例如使用async)来调用该函数并处理结果,只需知道它是否为{{1} } 或不。如果是这样,则您将使用awaitawait,其中async就是这样的await foo()函数,并且无论执行foo().then(result => { ... })还是可以的本身是否包含fooasync)。您无需查看其实现。我认为这称为“通话合同”。

为什么没有foo不能使用await

与上述基本原理类似,ECMAScript设计人员还可以选择将强制功能标记为async function foo() { return 1; },以便能够使用await。 ECMAScript可以允许任何函数使用async,但是,由于该语言不允许并行脚本执行,因此任何这样的函数都需要返回一个promise,以免严重影响事件处理。假设前者并保持这种想法,那么所有函数要么都将返回promise,要么只有那些使用async的函数才可以。 所有总是返回承诺的JavaScript函数至少会被ECMAScript设计人员认为是一个糟糕的主意,而您自己对问题的看法似乎也可以表达他们的观点。仅拥有使用await的那些函数总是返回promise,而一个更好的主意将意味着该函数返回的值的类型随该函数的实现而变化再次面对“调用合同”的原则,您将需要知道await是否使用await,以便知道如何处理调用此await的结果。在我看来,ECMAScript设计人员选择了反对它,这就是为什么必须使用foo明确标记使用await的函数的原因。

此外,两个规范都可以帮助实现更优化的语言运行时。


1 我可以交替使用“ ECMAScript”和“ JavaScript”-讨论区别(如果有的话)超出了答案的范围,在任何情况下都应与参数无关。

< / p>

答案 2 :(得分:0)

因为它使您可以选择做什么。异步函数调用下的代码可以立即运行,也可以等待异步完成。

您甚至可以拥有一些依赖于异步结果的代码来等待函数完成,而其他一些不关心结果的代码则可以立即运行:

someAsyncFunction().then(v => {
  // here goes the code that depends on v
}

// here goes the code that doesn't care about async completion

在只能按照您希望的方式运行的阻止语言中,您不能这样做。其余代码必须始终等待。