如何确定一个函数是否是异步的?

151

我需要将一个函数作为回调传递给另一个函数并执行。问题在于有时这个函数是异步的,例如:

async function() {
 // Some async actions
}

所以我想根据接收的函数类型执行await callback()或者callback()

有没有一种方法可以知道函数的类型?


11
不要试图检测它并根据获取的内容执行不同的操作。清楚地记录您是否支持返回 Promise 的回调函数,然后按照其实现。提示:如果您使用 await 等待非 Promise 对象,则会自动将其包装成 Promise。 - Bergi
4
异步的整个意义就在于不需要回调函数,对吧? - Felipe Valdes
@FelipeValdes - 我认为更准确的说法是:异步的重点不在于_管理_回调。我对 OP 的想法有些同感:如果要调用的函数不是异步的,那么直接调用该函数将产生值,而在调用上放置await将不必要地引入一个 promise 包装器,并且效率可能会略低。 - JohnRC
11个回答

119

理论

原生的async函数在被转换为字符串时可能会被识别出来:

asyncFn[Symbol.toStringTag] === 'AsyncFunction'

或者通过AsyncFunction构造函数:

const AsyncFunction = (async () => {}).constructor;

asyncFn instanceof AsyncFunction === true

这无法与Babel / TypeScript输出一起使用,因为asyncFn在转译代码中是常规函数,它是FunctionGeneratorFunction的实例,而不是AsyncFunction。为了确保它不会给转译代码中的生成器和常规函数带来错误的结果
const AsyncFunction = (async () => {}).constructor;
const GeneratorFunction = (function* () => {}).constructor;

(asyncFn instanceof AsyncFunction && AsyncFunction !== Function && AsyncFunction !== GeneratorFunction) === true

自从2017年原生的async函数正式引入Node.js之后,这个问题可能指的是async函数的Babel实现,该实现依赖于transform-async-to-generator来将async转换为生成器函数,并且还可以使用transform-regenerator将生成器转换为常规函数。
调用async函数的结果是一个Promise。根据提案,可以传递一个Promise或非Promise给await,所以await callback()是通用的。
只有少量边缘情况可能需要这样做。例如,原生的async函数在内部使用原生Promise,并且不会选择全局Promise,如果其实现发生了更改:
let NativePromise = Promise;
Promise = CustomPromiseImplementation;

Promise.resolve() instanceof Promise === true
(async () => {})() instanceof Promise === false;
(async () => {})() instanceof NativePromise === true;

这可能会影响函数行为(这是Angular和Zone.js promise实现的已知问题)。即使如此,最好检测函数返回值是否为预期的Promise实例,而不是检测函数是否为async,因为同样的问题适用于使用替代promise实现的任何函数,而不仅仅是async解决上述Angular问题的方法是将async返回值包装在Promise.resolve中)。

实践

从外部来看,async函数只是一个无条件返回本机promise的函数,因此应该像对待普通函数一样对待它。即使函数曾经被定义为async,它也可能在某个时刻被转译为常规函数。

可以返回promise的函数

在ES6中,可能返回Promise的函数可以使用Promise.resolve(处理同步错误)或包装Promise构造函数(处理同步错误):
Promise.resolve(fnThatPossiblyReturnsAPromise())
.then(result => ...);

new Promise(resolve => resolve(fnThatPossiblyReturnsAPromiseOrThrows()))
.then(result => ...);

在ES2017中,可以使用await来实现这一点(这就是问题示例应该编写的方式):
let result = await fnThatPossiblyReturnsAPromiseOrThrows();
...

应该返回一个Promise的函数

检查对象是否为Promise是另一个问题, 但通常不应过于严格或宽松以覆盖边缘情况。如果全局Promise被替换,instanceof Promise可能不起作用,Promise !== (async () => {})().constructor。这可能发生在Angular和非Angular应用程序之间接口。

需要是async的函数,即始终返回一个Promise的函数应首先被调用,然后检查返回值是否为Promise:

let promise = fnThatShouldReturnAPromise();
if (promise && typeof promise.then === 'function' && promise[Symbol.toStringTag] === 'Promise') {
  // is compliant native promise implementation
} else {
  throw new Error('async function expected');
}

TL;DR: async 函数不应该与返回 Promise 的普通函数区别对待。没有可靠的方法,也没有实际的理由来检测非原生转译的 async 函数。

这在我的端上无法工作。即使我有使用关键字“async”作为参数传递给“it()”规范的函数,AsyncFunction !== Function始终为false。顺便说一下,我正在使用TypeScript。你能否请看一下这个问题,并提供你的见解。我已经尝试了很多不同的方法,但还没有成功。 :( - Tums
@Tums 这是因为 AsyncFunction !== Function 的检查存在于避免误报。在转译代码中不会有真实的阳性,因为在转译代码中,async函数与常规函数没有区别。 - Estus Flask
我正在编写一个钩子函数,该函数接受一个对象、目标和钩子... 我如何知道是否需要等待? - Erik Aronesty
1
@ErikAronesty 能否提供一个一行代码的例子?如果一个值可以是 promise 或者不是 promise,你需要使用 await,它适用于 promises 和非 promises。这就是答案中最后一个片段展示的内容。 - Estus Flask
@EstusFlask: 看看我不能只是“等待”...因为那样我会改变挂钩函数的语义。 - Erik Aronesty
@ErikAronesty 是的,通常是这样做的。您可以检查函数的返回值是否为原生 Promise (result instanceof Promise) 或可 thenable 对象(result && typeof result.then === 'function'),而不是检查函数是否为 async - Estus Flask

53
只要只使用本地异步函数(这通常是情况),我更喜欢这种简单的方式:
theFunc.constructor.name == 'AsyncFunction'

1
这也比 stringify 更高效的优点。 - TOPKAT
1
鸭子类型的问题在于自定义函数可以通过此检查,“theFunc = new class AsyncFunction extends Function {}”。但是转译后的async函数则不行,“theFunc = () => __awaiter(void 0, void 0, void 0, function* () { })”。 - Estus Flask
3
当然,@EstusFlask,你完全是正确的。如果这是你的情况——你需要一个更复杂的解决方案。但在“现实世界”中(不是超级特殊或人工的情况下)——人们可以使用这个解决方案,而不是一些过度复杂的检查器。但应该意识到自己在说什么,感谢你的评论! - Alexander
1
为什么不像@theVoogie建议的那样使用=== 'AsyncFunction'呢? - themefield
任何人都可以定义 class AsyncFunction {} 并打破它,但我明白这很不可能。 - Lloyd
显示剩余2条评论

33

无论是 @rnd 还是 @estus 都是正确的。

但是为了回答这个问题并提供一个实际可行的解决方案,以下是具体步骤:

function isAsync (func) {
    const string = func.toString().trim();

    return !!(
        // native
        string.match(/^async /) ||
        // babel (this may change, but hey...)
        string.match(/return _ref[^\.]*\.apply/)
        // insert your other dirty transpiler check

        // there are other more complex situations that maybe require you to check the return line for a *promise*
    );
}

这是一个非常有价值的问题,我很生气有人对他进行了负面评价。这种类型检查的主要用例是用于库/框架/装饰器。

现在还处于初期阶段,我们不应该对有价值的问题进行负面评价。


1
我猜这个问题的问题在于它是XY问题。正如先前提到的,异步函数只返回承诺,因此根本不应该被检测到。顺便说一句,在缩小和转换代码时无法可靠地检测到它们,“_ref”将不存在。 - Estus Flask
1
一个微小的问题在于,很多时候人们会将 node 风格的回调函数包装成 promise 包装器以便与异步函数一起使用,因此该函数可能是异步的,但并不真正是异步的。无论哪种情况,await 都可以工作... 但涉及到异步生成器时可能会变得复杂。 - Tracker1
1
虽然如此,这仍然是一个有效的问题。在仅使用async和await的代码中,知道函数是否声明为async很重要,而async/await在底层实现方式则不相关。例如,如果您的API封装需要确保处理程序已声明为async,以便它可以抛出用户可以修复的错误,那么您需要对原始问题做出回答,而这个答案很好。所以补充这个答案:本地检查的另一种方法是fn.constructor.name,对于async函数将是AsyncFunction - Mike 'Pomax' Kamermans
2
记住,仅仅因为你目前没有意识到任何实际的情况,并不意味着不存在这种情况(https://github.com/pomax/socketless)。所以这是一个新的东西要学习:你可以发现一个函数是否是异步的,这意味着你可以编写代码来“做事情”与异步函数一起工作,而不影响常规函数(或反之亦然)。对于普通的代码而言,这有用吗?不,我也想不出你需要这个的场景。但是对于使用JS编写的代码分析、AST构建器或转换器本身而言,这是相当重要的。 - Mike 'Pomax' Kamermans
1
在一个 API 中遇到了这个问题,我们相当宽松地允许 function(resolve, reject)async function()Promise。如果没有涉及到转译器,后两者可以很容易地被检测出来。一旦涉及到转译器,就几乎不可能区分 asyncfunction(resolve, reject) 之间的差异了。不幸的是,由于代码压缩,提出的解决方案几乎无法工作。很遗憾,转译器没有利用 Promise polyfill,否则会更容易检测。 :/ - tresf
显示剩余4条评论

18

如果您正在使用NodeJS 10.x或更高版本

请使用本地实用函数

   util.types.isAsyncFunction(function foo() {});  // Returns false
   util.types.isAsyncFunction(async function foo() {});  // Returns true

请牢记以上答案中提到的所有问题。如果一个函数不小心返回了一个 Promise,它将会返回一个错误的负面结果。

此外(来自文档):

请注意,这只报告 JavaScript 引擎所看到的内容;特别是,如果使用了转译工具,则返回值可能与原始源代码不匹配。

但是,如果您在 NodeJS 10 中使用 async ,并且没有进行任何转义,那么这是一个不错的解决方案。


14

看起来 await 也可以用于普通函数。我不确定是否可以被视为“良好的实践”,但是这里是代码:

async function asyncFn() {
  // await for some async stuff
  return 'hello from asyncFn' 
}

function syncFn() {
  return 'hello from syncFn'
}

async function run() {
  console.log(await asyncFn()) // 'hello from asyncFn'
  console.log(await syncFn()) // 'hello from syncFn'
}

run()

await 将普通函数转换为 Promise。 - ahmelq

8

以下是David Walsh在他的博客文章中提供的简短实用方法:

const isAsync = myFunction.constructor.name === "AsyncFunction";

干杯!


1
这种方法已经在之前的答案中描述过了。 - Philippe-André Lorin

4

TL;DR

简短回答:在暴露AsyncFunction后使用instaceof。详见下文。

长篇回答:不要这样做。详见下文。

如何做到

您可以检测函数是否使用async关键字声明。

当您创建一个函数时,它会显示为Function类型:

> f1 = function () {};
[Function: f1]

你可以使用 instanceof 运算符进行测试:
> f1 instanceof Function
true

当您创建一个异步函数时,它会显示为AsyncFunction类型:
> f2 = async function () {}
[AsyncFunction: f2]

因此,我们可以使用 instanceof 进行测试:

> f2 instanceof AsyncFunction
ReferenceError: AsyncFunction is not defined

为什么会这样呢?因为AsyncFunction不是全局对象。请查看文档: 即使如您所见,它被列在 Reference/Global_Objects 下面……如果您需要轻松访问 AsyncFunction,您可以使用我的 unexposed 模块:

要获取本地变量:

const { AsyncFunction } = require('unexposed');

或者添加一个全局的AsyncFunction对象,与其他全局对象并列:

require('unexposed').addGlobals();

现在以上内容按预期工作:
> f2 = async function () {}
[AsyncFunction: f2]
> f2 instanceof AsyncFunction
true

为什么你不应该这样做

上面的代码将测试函数是否使用async关键字创建,但请记住,真正重要的不是函数是如何创建的,而是函数是否返回一个Promise。

无论在哪里使用此“async”函数:

const f1 = async () => {
  // ...
};

您还可以使用这个:

const f2 = () => new Promise((resolve, reject) => {
});

即使没有使用async关键字创建它,因此它不会与instanceof或其他答案中发布的任何其他方法匹配。

具体来说,请考虑以下内容:

const f1 = async (x) => {
  // ...
};

const f2 = () => f1(123);

f2只是使用硬编码参数的f1,在这里添加async并没有太多意义,尽管结果在所有方面与f1一样“异步”。

总结

因此,可以检查函数是否使用了async关键字创建,但要谨慎使用,因为当您检查它时,最有可能做错了什么。


根据"What I can understand with "Why you shouldn't do it"",我理解为检查一个函数是否被声明为async以了解其内部是否进行了一些异步/等待操作但未返回任何内容是可以的。 - Amit Kumar Gupta
1
@AmitGupta 它不会返回任何东西。它会返回一个 Promise。 - Estus Flask
如果你有一个混合了async/await(不需要知道promise的任何内容)和promise函数的代码库,那么这是你不应该做的事情。async/await的好处在于实现细节变得无关紧要:你不需要使用then().catch()来处理async函数,而是使用try/await。所以,如果你确实需要知道函数的类型,那么你完全可以检查它的类型,但不要使用instanceof:使用fn.constructor.name代替。如果它是AsyncFunction而不是Function,那么你就知道它是一个async函数。 - Mike 'Pomax' Kamermans

3
我希望能根据所接收的函数类型执行 await callback() 或 callback()。您可以始终使用 await 执行它,它会自动选择正确的方式:
async function main(callback) {
  let result = await callback(); // even if callback is not async
  // use 'result'
}

有没有办法知道函数的类型?也许你真正感兴趣的是函数的结果类型。Dariusz Filipiak的回答很好,但它可以更加简洁:
async function main(callback) {
  let result = callback();
  if (result instanceof Promise) {
    result = await result;
  }
  // use 'result'
}

1

不要测试你想要高效调用的函数是否是异步函数(即返回一个Promise),而是可以在不使用await的情况下直接调用它,检查返回值是否为promise,然后再进行等待:

let result = someFunctionWhichMayBeAsync()
if (typeof result?.then === 'function') {
   result = await result
}

这种方法适用于真正的异步函数和任何返回Promise (或 thenable) 的内容。唯一的注意点是如果返回一个具有不实际存在 thenable 方法的.then (但在那种情况下,调用awaitPromise.resolve()也会失败,因此您实际上没有更好的选择)。

可能与无条件await返回值相比,性能并没有提高多少,因为检查.then的值可能比await的开销更大-这里是基准测试


1
完整解决方案:同时处理异步和Promise

我经常交替使用Promises和async/await,因为它们基本相同。

Async/Await用于在异步函数中处理Promise。它基本上是promise的语法糖。它只是一个封装器,可以重新设计代码并使promise更易于阅读和使用。 来源:GeeksForGeeks

如果你需要一个帮助函数来确定一个值是否是一个异步函数(不调用它),或者一个值是否是一个返回Promise的函数,那么你已经来到了正确的文章。

在这个例子中,我将介绍三种不同的方法。

  1. 检查函数是否是async/await函数。
  2. 检查普通函数是否返回Promise。
  3. 两者兼顾。

处理异步函数

这个函数可以确定一个函数是否使用了async关键字来定义。

需要验证的示例函数

async function a() {}
const b = async () => {}

验证函数

function isAsyncFunction(f: unknown): boolean {
  return f && f.constructor.name === 'AsyncFunction'
}

处理 Promise 函数

此函数可以确定普通函数是否返回 Promise。为了评估给定函数是否返回 Promise,我们需要调用该函数并检查返回值。为避免多次调用同一函数,如果返回值是 Promise,则可以返回上述值,如果不是,则返回 false

要验证的示例函数

function a() { return new Promise(() => {}) }
const b = () => new Promise(() => {})

验证函数

function isPromiseFunction<T>(fn: any, ...params: any[]): Promise<T> | boolean {
    const isFunction = fn && typeof fn === 'function'
    const notAsyncFunction = fn.constructor.name !== 'AsyncFunction'
    if (isFunction && notAsyncFunction) {
        const value = fn(...params) || false
        if (value && value.constructor.name === 'Promise') {
            return value as Promise<T>
        }
    }
    return false
}

同时处理

因为AsyncFunctionPromise本质上是相同的,所以我们只需要检查它们是否都返回一个 Promise。

function isAsync<T>(fn: any, ...params: any[]): Promise<T> | boolean {
    const isFunction = fn && typeof fn === 'function'
    if (isFunction) {
        const value = fn(...params) || false
        if (value && value.constructor.name === 'Promise') {
            return value as Promise<T>
        }
    }
    return false
}

结论

异步函数更快、更干净,可以进行验证,而 Promise 函数需要调用才能进行验证。

测试函数(CodePen)

https://codepen.io/isakhauge/pen/gOGGQPY


我认为GeeksForGeeks这样说“Async/Await ...基本上是语法糖”有点误导人。在任何函数调用上使用await都会从根本上改变执行顺序。当await应用于同步函数调用时,结果是在await点处,尽管已经完成了调用的同步函数,但包含该调用的异步函数将返回一个未解决的Promise。 - JohnRC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接