Woher wissen, ob eine Funktion asynchron ist?

75

Ich muss eine Funktion an eine andere Funktion übergeben und als Rückruf ausführen. Das Problem ist, dass diese Funktion manchmal asynchron ist, wie:

async function() {
 // Some async actions
}

Ich möchte also ausführen await callback()oder callback()abhängig von der Art der Funktion, die es empfängt.

Gibt es eine Möglichkeit, den Typ der Funktion zu kennen?

Facundo Matteo
quelle
7
Versuchen Sie nicht, es zu erkennen und verschiedene Dinge zu tun, je nachdem, was Sie bekommen. Dokumentieren Sie klar, ob Sie Rückrufe unterstützen, die Versprechen zurückgeben oder nicht, und behandeln Sie sie dann als solche. (Hinweis: Wenn Sie awaitein Nicht-Versprechen haben, wird es trotzdem automatisch verpackt)
Bergi
3
Der springende Punkt bei Async ist, keine Rückrufe zu haben, oder?
Felipe Valdes

Antworten:

61

Theorie

Native asyncFunktionen können bei der Konvertierung in Zeichenfolgen identifiziert werden :

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

Oder vom AsyncFunctionKonstruktor:

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

asyncFn instanceof AsyncFunction === true

Dies funktioniert nicht mit der Babel / TypeScript-Ausgabe, da asyncFnes sich bei der regulären Funktion im transpilierten Code um eine Instanz von Functionoder GeneratorFunctionnicht handelt AsyncFunction. Um sicherzustellen, dass im transpilierten Code keine Fehlalarme für Generator- und reguläre Funktionen ausgegeben werden:

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

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

Da native asyncFunktionen 2017 offiziell in Node.js eingeführt wurden, bezieht sich die Frage wahrscheinlich auf die Babel-Implementierung von asyncFunktionen, die sich auf transform-async-to-generatordas Transpilieren asyncin Generatorfunktionen stützen und möglicherweise auch transform-regeneratorzum Transpilieren von Generatoren in reguläre Funktionen verwendet werden.

Das Ergebnis des asyncFunktionsaufrufs ist ein Versprechen. Dem Vorschlag zufolge kann ein Versprechen oder ein Nichtversprechen weitergegeben werden await, await callback()was universell ist.

Es gibt nur wenige Randfälle, in denen dies erforderlich sein kann. Zum Beispiel verwenden native asyncFunktionen native Versprechen intern und greifen nicht global auf, Promisewenn ihre Implementierung geändert wurde:

let NativePromise = Promise;
Promise = CustomPromiseImplementation;

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

Dies kann das Funktionsverhalten beeinflussen (dies ist ein bekanntes Problem für die Implementierung von Angular und Zone.js Versprechen ). Selbst dann ist es vorzuziehen, zu erkennen, dass der Funktionsrückgabewert nicht als PromiseInstanz erwartet wird, anstatt zu erkennen, dass es sich um eine Funktion handelt async, da dasselbe Problem auf jede Funktion anwendbar ist, die eine alternative Versprechenimplementierung verwendet, und nicht nur async( die Lösung für dieses Winkelproblem besteht darin, die asyncRückgabe zu verpacken Wert mit Promise.resolve).

Trainieren

Von außen ist asyncFunktion nur eine Funktion, die bedingungslos native Versprechen zurückgibt, daher sollte sie wie eine behandelt werden. Selbst wenn eine Funktion einmal definiert wurde async, kann sie irgendwann transpiliert werden und zu einer regulären Funktion werden.

Eine Funktion, die ein Versprechen zurückgeben kann

In ES6 kann eine Funktion, die möglicherweise ein Versprechen zurückgibt, mit Promise.resolve(lässt synchrone Fehler) oder einem umschlossenen PromiseKonstruktor (behandelt synchrone Fehler) verwendet werden:

Promise.resolve(fnThatPossiblyReturnsAPromise())
.then(result => ...);

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

In ES2017 geschieht dies mit await(so soll das Beispiel aus der Frage geschrieben werden):

let result = await fnThatPossiblyReturnsAPromiseOrThrows();
...

Eine Funktion, die ein Versprechen zurückgeben sollte

Die Überprüfung, ob ein Objekt ein Versprechen ist, ist eine separate Frage , sollte jedoch im Allgemeinen nicht zu streng oder locker sein, um Eckfälle abzudecken. instanceof Promisefunktioniert möglicherweise nicht, wenn global Promiseersetzt wurde Promise !== (async () => {})().constructor. Dies kann passieren, wenn Angular- und Nicht-Angular-Anwendungen eine Schnittstelle bilden.

Eine Funktion, die erforderlich ist async, dh immer ein Versprechen zurückzugeben, sollte zuerst aufgerufen werden. Anschließend wird der zurückgegebene Wert als Versprechen überprüft:

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: asyncFunktionen sollten nicht von regulären Funktionen unterschieden werden, die Versprechen zurückgeben. Es gibt keinen zuverlässigen Weg und keinen praktischen Grund, nicht native transpilierte asyncFunktionen zu erkennen .

Estus Flask
quelle
Das funktioniert an meinem Ende nicht. AsyncFunction !== Functionist immer falsch, obwohl ich Funktionen mit einem Schlüsselwort habe, asyncdas als Argument an eine it()Spezifikation übergeben wird. Ich benutze übrigens Typescript. Könnten Sie sich bitte diese Frage ansehen und Ihre Erkenntnisse liefern. Ich habe so viele verschiedene Wege ausprobiert, aber es ist mir noch nicht gelungen. :(
Tums
@Tums Das liegt daran AsyncFunction !== Function, dass die Überprüfung dazu dient, Fehlalarme zu vermeiden . Im transpilierten Code gibt es keine echten Positiven , da sich die asyncFunktionen nicht von den regulären im transpilierten Code unterscheiden.
Estus Flask
Ich schreibe eine Hook-Funktion, die Funktion nimmt ein Objekt, ein Ziel und einen Hook ... woher weiß ich, ob ich warten muss?
Erik Aronesty
@ErikAronesty Können Sie ein einzeiliges Beispiel liefern? Wenn ein Wert ein Versprechen sein kann oder nicht, müssen Sie await, er funktioniert für Versprechen und Nichtversprechen. Dies zeigt der letzte Ausschnitt in der Antwort.
Estus Flask
@EstusFlask: stackoverflow.com/questions/10273309/… Sehen Sie, wie ich nicht einfach "warten" kann ... weil ich dann die Semantik der Hook-Funktion ändern würde.
Erik Aronesty
26

Ich bevorzuge diesen einfachen Weg:

theFunc.constructor.name == 'AsyncFunction'
Alexander
quelle
1
Dies hat auch den Vorteil, performanter zu sein als ein Stringify :)
538ROMEO
Das Problem bei der Eingabe von Enten besteht darin, dass die benutzerdefinierte Funktion diese Prüfung besteht theFunc = new class AsyncFunction extends Function {}. Aber transpiled asyncFunktion nicht , theFunc = () => __awaiter(void 0, void 0, void 0, function* () { }).
Estus Flask
1
Natürlich @EstusFlask, du hast vollkommen recht. Wenn es Ihr Fall ist, brauchen Sie eine komplexere Lösung. Aber in einer "realen Welt" (keine speziellen oder künstlichen Fälle) könnte man diese Lösung anstelle von Overkill-Monsterprüfern verwenden. Aber man sollte sich bewusst sein, was Sie sagen, danke für Ihren Kommentar!
Alexander
Warum nicht === 'AsyncFunction'wie von @theVoogie vorgeschlagen verwenden?
Themenfeld
@Alexander, in einer realen Welt geben nicht-asynchrone Funktionen immer wieder Versprechen zurück, genau wie asynchrone Funktionen.
Cababunga
24

Sowohl @rnd als auch @estus sind korrekt.

Aber um die Frage mit einer tatsächlich funktionierenden Lösung zu beantworten, gehen Sie

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*
    );
}

Dies ist eine sehr berechtigte Frage, und ich bin verärgert, dass ihn jemand abgelehnt hat. Der Hauptanwendungsfall für diese Art der Überprüfung ist eine Bibliothek / ein Framework / ein Dekorateur.

Dies sind frühe Tage, und wir sollten GÜLTIGE Fragen nicht ablehnen .

Chad Scira
quelle
Ich denke, das Problem mit dieser Frage ist, dass es ein XY-Problem ist. Wie bereits erwähnt, geben asynchrone Funktionen nur Versprechen zurück, sodass sie überhaupt nicht erkannt werden sollten. Übrigens können sie im minimierten transpilierten Code nicht zuverlässig erkannt werden und _refsind nicht da.
Estus Flask
1
Ein kleines Problem darüber hinaus ist, dass Benutzer häufig Rückrufe im Knotenstil in Versprechen-Wrapper für asynchrone Funktionen einbinden, sodass die Funktion möglicherweise in angemessenem Maße asynchron, aber nicht wirklich asynchron ist. Warten kann in beiden Fällen funktionieren ... wo es kompliziert werden könnte, sind asynchrone Generatoren.
Tracker1
1
Dies ist jedoch immer noch eine gültige Frage. In Code, der nur Async und Warten verwendet, ist es wichtig zu wissen, ob eine Funktion als asynchron deklariert wurde oder nicht, und es ist unerheblich, wie Async / Warten unter der Haube implementiert wurde. Wenn Ihr API-Wrapper beispielsweise sicherstellen muss, dass ein Handler als asynchron deklariert wurde, damit er einen Fehler auslösen kann, den der Benutzer beheben kann, möchten Sie eine Antwort auf die ursprüngliche Frage, und diese Frage funktioniert einwandfrei. Um diese Antwort zu ergänzen: Eine andere Möglichkeit, nativ zu prüfen, ist fn.constructor.namedie AsyncFunctionfür asynchrone Funktionen.
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Die Frage ergab sich aus einem falschen Verständnis der awaitSemantik. Es spielt keine Rolle, ob sich eine Funktion asyncin einem mir bekannten praktischen Szenario befindet. asyncist nur eine Funktion, die bedingungslos native Versprechen zurückgibt - und wie eine behandelt werden sollte. asynckann irgendwann transpiliert werden, dies sollte eine App nicht ruinieren. Für das von Ihnen beschriebene Szenario ist es richtig, dass ein Wrapper eine Funktion aufruft und einen Wert als vielversprechend und nicht als funktionierend festlegt async. Wenn ungültige Handler so schnell wie möglich verhindert werden sollen, muss dies zur Entwurfszeit mit TS / Flow
Estus Flask
Denken Sie daran, nur weil Sie kein praktisches Szenario kennen, heißt das nicht, dass es keines gibt . Das ist also etwas Neues zu lernen: Sie können feststellen, ob eine Funktion asynchron ist oder nicht. Dies bedeutet, dass Sie Code schreiben können, der mit oder zu asynchronen Funktionen "Dinge tut", während Sie reguläre Funktionen in Ruhe lassen (oder umgekehrt). Ist es nützlich für normalen Code? Nein, ich kann mir auch kein Szenario vorstellen, in dem Sie das brauchen würden. Aber ist das wichtig für Code-Analysen, AST-Builder oder Transpiler, die selbst in JS geschrieben sind? Ja, eigentlich ziemlich wichtig.
Mike 'Pomax' Kamermans
10

Falls Sie NodeJS 10.x oder höher verwenden

Verwenden Sie die native util-Funktion .

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

Beachten Sie jedoch alle Bedenken der oben genannten Antworten. Eine Funktion, die nur versehentlich ein Versprechen zurückgibt, gibt ein falsches Negativ zurück.

Und obendrein (aus den Dokumenten):

Beachten Sie, dass dies nur zurückmeldet, was die JavaScript-Engine sieht. Insbesondere stimmt der Rückgabewert möglicherweise nicht mit dem ursprünglichen Quellcode überein, wenn ein Transpilationswerkzeug verwendet wurde.

Aber wenn Sie asyncin NodeJS 10 und keine Transiplation verwenden. Dies ist eine schöne Lösung.

Segers-Ian
quelle
4

TL; DR

Kurze Antwort: instaceofNach dem Belichten verwenden AsyncFunction - siehe unten.

Lange Antwort: Tu das nicht - siehe unten.

Wie es geht

Sie können feststellen, ob eine Funktion mit dem asyncSchlüsselwort deklariert wurde

Wenn Sie eine Funktion erstellen, wird angezeigt, dass es sich um eine Funktion handelt:

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

Sie können es mit dem instanceofBediener testen :

> f1 instanceof Function
true

Wenn Sie eine asynchrone Funktion erstellen, wird angezeigt, dass es sich um eine AsyncFunction handelt:

> f2 = async function () {}
[AsyncFunction: f2]

man könnte also erwarten, dass es auch getestet werden kann mit instanceof:

> f2 instanceof AsyncFunction
ReferenceError: AsyncFunction is not defined

Warum das? Weil die AsyncFunction kein globales Objekt ist. Siehe die Dokumente:

obwohl, wie Sie sehen können, es unter Reference/Global_Objects...

Wenn Sie einen einfachen Zugang zum benötigen AsyncFunction, können Sie mein unexposedModul verwenden:

um entweder eine lokale Variable zu erhalten:

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

oder um ein globales AsyncFunctionneben anderen globalen Objekten hinzuzufügen :

require('unexposed').addGlobals();

und jetzt funktioniert das oben genannte wie erwartet:

> f2 = async function () {}
[AsyncFunction: f2]
> f2 instanceof AsyncFunction
true

Warum solltest du es nicht tun?

Der obige Code testet, ob die Funktion mit dem asyncSchlüsselwort erstellt wurde. Beachten Sie jedoch, dass es nicht darauf ankommt, wie eine Funktion erstellt wurde, sondern ob eine Funktion ein Versprechen zurückgibt.

Überall, wo Sie diese "asynchrone" Funktion verwenden können:

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

Sie können dies auch verwenden:

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

obwohl es nicht mit dem asyncSchlüsselwort erstellt wurde und daher nicht mit instanceofoder mit einer anderen Methode übereinstimmt , die in anderen Antworten veröffentlicht wurde .

Beachten Sie insbesondere Folgendes:

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

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

Das f2ist nur f1mit fest codierten Argumenten und es macht nicht viel Sinn, asynchier hinzuzufügen , obwohl das Ergebnis genauso "asynchron" sein wird wie f1in jeder Hinsicht.

Zusammenfassung

Es ist also möglich zu überprüfen, ob eine Funktion mit dem asyncSchlüsselwort erstellt wurde, aber verwenden Sie sie mit Vorsicht, da Sie beim Überprüfen höchstwahrscheinlich etwas falsch machen.

rsp
quelle
Was ich unter "Warum Sie es nicht tun sollten" verstehen kann, ist in Ordnung zu überprüfen, ob eine Funktion mit deklariert ist, um asynczu wissen, ob sie eine asynchrone / wartende Operation im Inneren ausführt, aber nichts zurückgibt.
Amit Kumar Gupta
1
@AmitGupta Es gibt nichts zurück. Es gibt ein Versprechen zurück.
Estus Flask
Wenn Sie eine Code - Basis , dass Mischungen async / await haben Funktionen und verspricht (was nichts über Versprechungen verlangt zu wissen), wirklich das ist die Sache , sollten Sie nicht tun. Das Schöne an async / await ist, dass die Implementierungsdetails irrelevant werden: Sie haben keine then().catch()asynchrone Funktion, try/awaitsondern diese. Also ja, Sie sollten unbedingt den Typ der Funktion überprüfen, wenn Sie unbedingt wissen möchten, ob sie asynchron ist oder nicht, aber nicht mit instanceof: Verwenden Sie fn.constructor.namestattdessen. Wenn dies nicht der AsyncFunctionFall ist Function, wissen Sie, dass es sich um eine asynchrone Funktion handelt.
Mike 'Pomax' Kamermans
4

Es scheint, dass awaitdies auch für normale Funktionen verwendet werden kann. Ich bin nicht sicher, ob es als "gute Praxis" angesehen werden kann, aber hier ist es:

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()
Gyo
quelle
1

Hei,

Hier ist ein Ansatz von David Walsh in seinem Blogpost :

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

Prost!

theVoogie
quelle
0

Sie können zu Beginn davon ausgehen, dass ein Rückruf vielversprechend ist:

export async function runSyncOrAsync(callback: Function) {

  let promisOrValue = callback()
  if (promisOrValue instanceof Promise) {
    promisOrValue = Promise.resolve(promisOrValue)
  }
  return promisOrValue;
}

und sie in Ihrem Code können Sie dies tun:

await runSyncOrAsync(callback)

das wird Ihr Problem mit unwissendem Rückruftyp lösen ....

Dariusz Filipiak
quelle