Як дізнатися, чи є функція асинхронною?


77

Я повинен передати функцію іншій функції і виконати її як зворотний виклик. Проблема в тому, що інколи ця функція є асинхронною, наприклад:

async function() {
 // Some async actions
}

Тому я хочу виконати await callback()або callback()залежно від типу функції, яку він отримує.

Чи є спосіб дізнатись тип функції ??


7
Не намагайтеся його виявити і робити різні дії залежно від того, що ви отримаєте. Чітко задокументуйте, чи підтримуєте ви зворотні дзвінки, що повертають обіцянки, чи ні, а потім розглядайте їх як такі. (Підказка: якщо ви awaitне обіцяєте, це все одно автоматично оберне)
Бергі,

3
вся суть асинхронізації полягає у відсутності зворотних викликів, так?
Феліпе Вальдес,

Відповіді:


65

Теорія

Власні asyncфункції можуть бути ідентифіковані при перетворенні в рядки :

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

Або за AsyncFunctionконструктором:

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

asyncFn instanceof AsyncFunction === true

Це не буде працювати з вихідними даними Babel / TypeScript, оскільки asyncFnце звичайна функція у перекладеному коді, це екземпляр Functionчи GeneratorFunctionні AsyncFunction. Щоб переконатися, що це не дасть помилкових спрацьовувань для генератора та звичайних функцій у перекладеному коді:

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

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

Оскільки власні asyncфункції були офіційно представлені в Node.js в 2017 році, питання, ймовірно, стосується реалізації asyncфункції Babel , яка покладається на transform-async-to-generatorтранпіляцію asyncдо функцій генератора, а також може використовуватись transform-regeneratorдля транпіляції генератора до звичайних функцій.

Результатом asyncвиклику функції є обіцянка. Відповідно до пропозиції , обіцянка чи не обіцянка може бути передана await, тому await callback()є універсальною.

Є лише кілька випадків, коли це може знадобитися. Наприклад, власні asyncфункції використовують власні обіцянки внутрішньо і не піднімають глобальні, Promiseякщо їх реалізація була змінена:

let NativePromise = Promise;
Promise = CustomPromiseImplementation;

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

Це може вплинути на поведінку функції (це відома проблема реалізації обіцянок Angular та Zone.js ). Навіть тоді переважно виявити, що значення функції, що повертається, не є очікуваним, Promiseзамість того, щоб виявити, що функцією є async, оскільки ця сама проблема застосовна до будь-якої функції, яка використовує альтернативну реалізацію обіцянки, а не тільки async( рішення вказаної проблеми Angular полягає в обгортанні asyncreturn значення з Promise.resolve).

Практика

Зовні asyncфункція - це просто функція, яка безумовно повертає рідну обіцянку, тому до неї слід ставитись як до такої. Навіть якщо функція колись була визначенаasync , вона в якийсь момент може бути перетворена і стати регулярною функцією.

Функція, яка може повернути обіцянку

У ES6 функція, яка потенційно повертає обіцянку, може використовуватися з Promise.resolve(допускає синхронні помилки) або обгорнутим Promiseконструктором (обробляє синхронні помилки):

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

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

У ES2017 це зроблено за допомогою await(так слід писати приклад із запитання):

let result = await fnThatPossiblyReturnsAPromiseOrThrows();
...

Функція, яка повинна повертати обіцянку

Перевірка того, чи є об’єкт обіцянкою, - це питання окремого питання , але, як правило, воно не повинно бути занадто суворим або вільним, щоб охопити кутові справи. instanceof Promiseможе не працювати, якщо глобальний Promiseбув замінений, Promise !== (async () => {})().constructor . Це може статися, коли Angular та non-Angular програми взаємодіють.

asyncСпершу потрібно викликати функцію, яка вимагає бути , тобто завжди повертати обіцянку, потім повертається значення перевіряється як обіцянку:

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функції не слід відрізняти від звичайних функцій, які повертають обіцянки. Не існує надійного способу та немає практичних причин для виявлення неприродних перекладених asyncфункцій.


Це не працює з мого боку. AsyncFunction !== Functionзавжди хибне, хоча у мене є функції з ключовим словом, asyncпереданими як аргумент it()специфікації. Я до речі використовую Typescript. Не могли б ви поглянути на це питання та надати свою інформацію. Я намагався так багато різних способів, але поки не досяг успіху. :(
Тумс

@Tums Це тому, що AsyncFunction !== Functionперевірка існує, щоб уникнути помилкових спрацьовувань . У перекладеному коді не буде справжніх позитивів, оскільки asyncфункції не перешкоджають звичайним в перекладеному коді.
Estus Flask

Я пишу функцію хука, функція приймає об’єкт, ціль і хук ... як дізнатися, чи потрібно чекати?
Ерік Аронесті,

@ErikAronesty Чи можете ви навести однокласний приклад? Якщо цінність може бути обіцянкою чи не обіцянкою, вам потрібно await, це працює для обіцянок та не обіцянок. Ось що показує останній фрагмент відповіді.
Estus Flask

@EstusFlask: stackoverflow.com/questions/10273309 / ... Подивіться , як я можу не просто «Await» ... бо тоді я б зміни семантики гачкуватої функції.
Ерік Аронесті,

27

Я віддаю перевагу такому простому способу:

theFunc.constructor.name == 'AsyncFunction'

1
Це також має перевагу в тому, щоб бути більш продуктивним, ніж
stringify

Проблема з качиної типізацією є те , що призначена для користувача функція проходить цю перевірку, theFunc = new class AsyncFunction extends Function {}. Але transpiled asyncфункція не робить, theFunc = () => __awaiter(void 0, void 0, void 0, function* () { }).
Estus Flask

1
Звичайно @EstusFlask, ви абсолютно праві. Якщо це ваш випадок - вам потрібно більш складне рішення. Але у "реальному світі" (не надто особливих чи штучних випадках) - можна було б використовувати це рішення, замість того, щоб переборщити монстрів. Але треба бути в курсі того, що ви говорите, дякую за ваш коментар!
Олександр

Чому б не використовувати === 'AsyncFunction'те, що запропонував @theVoogie?
themefield

@Alexander, в реальному світі несинхронні функції постійно повертають обіцянки, як і асинхронні функції.
кабабунга

24

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

Це дуже слушне запитання, і я засмучений тим, що хтось голосував за нього. Основним варіантом використання цього типу перевірки є бібліотека / фреймворк / декоратори.

Ці перші дні, і ми не повинні downvote VALID питань.


Я думаю, проблема цього питання полягає в тому, що це проблема XY. Як вже було згадано, асинхронні функції просто повертають обіцянки, тому їх взагалі не слід виявляти. До речі, їх неможливо надійно виявити у зменшеному перекладеному коді, _refїх там не буде.
Estus Flask

1
Невелика проблема, окрім цього, полягає в тому, що багато разів люди обертають зворотні виклики у стилі вузлів у обіцянки-обгортки для використання з асинхронними функціями, тому функція може бути асинхронною в розумному ступені, але насправді не асинхронною. await може працювати в будь-якому випадку ... де це може ускладнитися - це асинхронні генератори.
Tracker1

1
Однак це все ще актуальне питання. У коді, який використовує лише асинхронізацію та очікування, важливо знати, чи була функція оголошена асинхронізацією чи ні, і не має значення, як асинхронізувати / чекати під капотом. Наприклад, якщо вашій обгортці API потрібно переконатись, що обробник оголошено асинхронним, щоб він міг видати помилку, яку користувач може виправити, тоді вам потрібна відповідь на вихідне запитання, і це буде чудово. Таким чином , щоб додати до цього відповіді: ще один спосіб перевірити це спочатку fn.constructor.name, який буде AsyncFunctionдля функції асинхронної.
Майк 'Помакс' Камерманс

@ Mike'Pomax'Kamermans Питання виникло через неправильне розуміння awaitсемантики. Не має значення, чи функція знаходиться asyncв якомусь практичному сценарії, який мені відомий. asyncце просто функція, яка безумовно повертає рідну обіцянку - і до неї слід ставитись як до однієї. asyncможе перетворитися в якийсь момент, це не повинно зіпсувати програму. Для сценарію, який ви описуєте, правильно, щоб обгортка викликала функцію і стверджувала, що значення є обіцяним, а не стверджує, що функція є async. Якщо потрібно якомога швидше запобігти недійсним обробникам, це має бути застосовано під час проектування за допомогою TS / Flow
Estus Flask,

Пам'ятайте, що те, що ви не знаєте жодного практичного сценарію, не означає, що таких немає . Отже, це щось нове для вивчення: ви можете виявити, чи є функція асинхронною чи ні, а це означає, що ви можете написати код, який буде «робити щось» з асинхронними функціями, залишаючи звичайні функції наодинці (або навпаки). Чи корисно це для звичайного коду? Ні, я також не можу придумати сценарій, за якого вам це потрібно. Але чи це важливо для аналізу коду, розробників AST або транпіляторів, які самі написані в JS? Так: насправді досить важливо.
Майк 'Помакс' Камерманс,

11

Якщо ви використовуєте NodeJS 10.x або пізнішої версії

Використовуйте рідну функцію util .

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

Майте на увазі всі занепокоєння зверху. Функція, яка просто випадково повертає обіцянку, поверне помилково негативний результат.

І на додачу до цього (з документації):

Зверніть увагу, що це повідомляє лише про те, що бачить механізм JavaScript; зокрема, повернене значення може не збігатися з вихідним вихідним кодом, якщо використовувався інструмент транпіляції.

Але якщо ви використовуєте asyncв NodeJS 10 і ніякої пересадки немає. Це приємне рішення.


4

TL; DR

Коротка відповідь: Використовуйте instaceofпісля викриття AsyncFunction - див. Нижче.

Довга відповідь: Не робіть цього - дивіться нижче.

Як це зробити

Ви можете визначити, чи була функція оголошена за допомогою async ключовим словом

Коли ви створюєте функцію, вона показує, що це тип Функція:

> 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ключовим словом, але майте на увазі, що насправді важливо не те, як була створена функція, а повертає чи не обіцянка.

Скрізь, де ви можете використовувати цю функцію "асинхронізація":

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

Ви також можете використовувати це:

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

навіть незважаючи на те, що воно не було створене за asyncключовим словом, і, отже, не буде узгоджуватися instanceofз будь-яким іншим способом, розміщеним в інших відповідях .

Зокрема, розглянемо це:

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

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

Це f2просто f1з жорстко закодованим аргументом, і немає сенсу додавати asyncсюди, хоча результат буде настільки ж "асинхронним", як і f1в усіх відношеннях.

Резюме

Отже, можна перевірити, чи функцію було створено за asyncключовим словом, але використовувати її слід з обережністю, оскільки ви, перевіряючи її, швидше за все робите щось не так.


Що я можу зрозуміти з "Чому ви не повинні це робити", добре перевірити, чи оголошена функція, asyncщоб знати, чи виконує вона якусь операцію async / await всередині, але нічого не повертає.
Аміт Кумар Гупта,

1
@AmitGupta Це нічого не повертає. Це повертає обіцянку.
Estus Flask

Якщо у вас є кодова база, яка поєднує функції async / await (що вимагає нічого не знати про обіцянки) та функції обіцянки, насправді це те, що ви не повинні робити. Найприємніше в async / await полягає в тому, що деталі реалізації стають неактуальними: ви не then().catch()використовуєте асинхронну функцію, try/awaitа замість неї. Отже, так, вам слід повністю перевірити тип функції, якщо вам легітимно потрібно знати, асинхронізована вона чи ні, але не за допомогою instanceof: use fn.constructor.nameзамість цього. Якщо це AsyncFunctionзамість Function, ви знаєте, що це функція асинхронізації.
Майк 'Помакс' Камерманс

4

Здається, це 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()

2

Хей,

Ось підхід, запропонований Девідом Уолшем у своєму блозі :

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

На здоров’я!


0

На початку можна припустити, що зворотний дзвінок є обіцяним:

export async function runSyncOrAsync(callback: Function) {

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

і їх у своєму коді ви можете зробити це:

await runSyncOrAsync(callback)

що вирішить вашу проблему з невідомим типом зворотного дзвінка ....

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.