Почему мне нужно ожидать асинхронную функцию, когда она предположительно не возвращает обещание?

#reactjs #promise #asyncstorage

#javascript #node.js #ecmascript-6 #обещание #async-await

Вопрос:

Рассмотрим этот код:

 async function load() {
  const data = await new Promise(resolve => {
    setTimeout(() => resolve([1, 2, 3]), 10);
  }).then(data => data.map(i => i * 10));
  console.log(`Data inside the function: ${JSON.stringify(data)}`);
  return data;
}

function main() {
  const data = load();
  console.log(`Loaded data: ${JSON.stringify(data)}`);
}

main();
 

Это результат, который я получаю:

 Loaded data: {}
Data inside the function: [10,20,30]
 

Но если я изменю код на это:

асинхронная функция load() {
const data = ожидает нового обещания(resolve => {
 setTimeout(() => разрешить([1, 2, 3]), 10);
 }).затем(data => data.map(i => i * 10));
 console.log(`Данные внутри функции: ${JSON.stringify(data)}`);
 возвращать данные;
}

асинхронная функция main() {
const data = await load();
console.log(`Загруженные данные: ${JSON.stringify(data)}`);
}

main();

Я получу это:

 Data inside the function: [10,20,30]
Loaded data: [10,20,30]
 

Я в замешательстве, потому что, основываясь на документации, await следует приостановить выполнение до тех пор, пока обещание не будет выполнено. В этом случае первый пример должен возвращаться data в виде массива. Но, как вы можете видеть, она возвращает a Promise , и я понятия не имею, почему !?

В то же время в документации есть эта часть, о которой я не понимаю, о чем она говорит:

Ожидание может разделить поток выполнения, позволяя вызывающей функции await возобновить выполнение до отложенного продолжения функции await . После того, как ожидание откладывает продолжение своей функции, если это первое ожидание, выполняемое функцией, немедленное выполнение также продолжается, возвращая вызывающей функции ожидающее обещание для завершения функции await и возобновления выполнения этого вызывающего.

Мне кажется, что await это работает, только если все функции в вашем коде async , что смешно, поскольку, если я использую функцию из другого модуля, как я должен знать, является ли это async или нет !? Или, может быть, мне следует занять осторожную сторону и всегда вызывать все функции с помощью an await , независимо от того, есть они async или нет!!!

[ОБНОВЛЕНИЕ]

Спасибо всем, кто участвовал и предоставил мне понимание. Но я все еще не понимаю, как мне следует использовать await and async . Должен ли я всегда вызывать все мои функции с помощью an await ?

Допустим, я пишу код, состоящий из нескольких функций в нескольких файлах. Если я в конечном итоге использую библиотеку, которая возвращает a Promise или это async функция, должен ли я отслеживать все мои вызовы функций от асинхронной точки до точки входа приложения и добавлять an await перед всеми вызовами функций после их async выполнения? Или, может быть, я должен просто привыкнуть вызывать все свои функции с помощью an await независимо от того, есть они async или нет?

Комментарии:

1. Что касается вашего редактирования: нет смысла использовать await что-либо, что не является обещанием. Предположительно, когда вы вызываете функцию из библиотеки и пытаетесь использовать ее возвращаемое значение, вы уже знаете, возвращает ли она обещание или нет. Документация для библиотеки скажет вам, и вам будет трудно узнать, что функция вообще существует, не выяснив, возвращает ли она обещание.

2. Когда вы вызываете функцию, которая возвращает обещание, вы можете использовать await , но вам не обязательно. Возможно, вы также захотите использовать объект Promise напрямую

3. Эту часть я понимаю, и я принимаю, что я должен знать возвращаемое значение любой функции, которую я вызываю. На данный момент моя проблема с реализацией async и await является скорее хорошей практикой. Вы всегда предполагаете, что код, который вы будете писать в будущем, всегда будет возвращать a Promise ? Если нет, что произойдет, если некоторый код, который должен быть синхронным, окажется асинхронным? Вам нужно вернуться к точке входа вашего приложения и дождаться всех вызовов, верно? Как избежать таких изменений?

4. Обычно вы знаете, нужно ли вашему коду ожидать чего-то асинхронного или нет, прежде чем вы его напишете, и если это не нужно, маловероятно, что это понадобится в будущем. Если вам действительно нужно когда-либо изменить существующий интерфейс с синхронизации на асинхронный, вы правы в том, что он оказывает своего рода каскадный лавинный эффект на каждого вызывающего абонента и вызывающих абонентов и так далее. Это относится и к обычным функциям обратного вызова. Хотя это кажется маловероятным сценарием; Я не думаю, что мне когда-либо приходилось менять существующий интерфейс на что-то с синхронного на асинхронный.

5. Вам повезло 🙂 это было то, что я делал последние несколько часов

Ответ №1:

Все async функции возвращают обещание. Все из них.

Это обещание в конечном итоге будет разрешено с любым значением, которое вы возвращаете из асинхронной функции.

await блокирует только выполнение внутри async функции. Он не блокирует ничего за пределами функции. Концептуально асинхронная функция начинает выполняться, и как только она попадает в await инструкцию, она немедленно возвращает невыполненное обещание (в состоянии ожидания) от функции, а внешний исполняющий мир получает это обещание и продолжает выполнять.

Некоторое время спустя внутреннее обещание, которое редактировалось await , разрешится, а затем выполнение остальных внутренних компонентов функции продолжится. В конечном итоге внутренние компоненты функции завершатся и вернут значение. Это приведет к разрешению обещания, которое было возвращено из функции с этим возвращаемым значением.

К вашему сведению, в вашей функции много лишнего load() . Вы можете изменить это следующим образом:

 async function load() {
  const data = await new Promise(resolve => {
    setTimeout(() => resolve([1, 2, 3]), 10);
  }).then(data => data.map(i => i * 10));
  console.log(`Data inside the function: ${JSON.stringify(data)}`);
  return data;
}
 

к этому:

 function load() {
    return new Promise(resolve => {
        setTimeout(() => resolve([1, 2, 3]), 10);
    }).then(data => data.map(i => i * 10));
}
 

Затем используйте его следующим образом:

 load().then(result => {
    console.log(result);
});
 

Или я предпочитаю инкапсулировать ручное создание promise в их собственную функцию следующим образом:

 function delay(t, v) {
    return new Promise(resolve => {
        setTimeout(resolve.bind(null, v), t);
    });
}

function load() {
    return delay(10, [1, 2, 3]).then(data => data.map(i => i * 10));
}
 

And, it turns out this little delay() function is generally useful in lots of places where you want to delay a promise chain.


Thanks to everyone participating and providing me with insight. But I’m still confused how should I be using await and async.

First off, most of the time you only mark a function async if you need to use await inside the function.

Second, you most commonly use await (from within an async function) when you have multiple asynchronous operations and you want to sequence them — often because the first one provides a result that is used as input to the second. You can use await when all you have is a single asynchronous operation, but it doesn’t really offer much of an advantage over a simple .then() .

Here are a few examples of good reasons to use async/await :

Sequencing multiple asynchronous operations

Imagine you have getFromDatabase() , getTheUrl() and getTheContent() that are all asynchronous. If any fails, you would want to just reject the returned promise with the first error.

Here’s how this looks without async/await:

 function run() {
    return getFromDatabase(someArg).then(key => {
        return getTheURL(key);
    }).then(url => {
        return getTheContent(url);
    }).then(content => {
         // some final processing
         return finalValue;
    });
}
 

Here’s how this looks with async/await :

 async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}
 

В обоих случаях функция возвращает обещание, которое разрешается с помощью finalValue, поэтому вызывающая сторона использует эти две реализации одинаково:

 run(someArg).then(finalValue => {
    console.log(finalValue);
}).catch(err => {
    console.log(err);
});
 

Но вы заметите, что async/await реализация имеет более сериализованный, синхронный вид и больше похожа на неасинхронный код. Многие считают, что это проще писать, легче читать и легче поддерживать. Чем больше у вас обработки между шагами, включая ветвление, тем больше преимуществ получает async/await версия.

Автоматический перехват как отклоненных обещаний, так и синхронных исключений

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

Само собой разумеется, что если вы вручную возвращаете обещание из async функции, и это обещание отклоняется, то обещание, возвращенное из async функции, будет отклонено.

Но также, если вы используете await и любое ожидаемое вами обещание отклоняется, и у вас нет a .catch() в обещании и нет a try/catch вокруг него, тогда обещание, возвращаемое функцией, будет автоматически отклонено. Итак, вернемся к нашему предыдущему примеру этого:

 async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}
 

Если какое-либо из трех редактируемых обещаний await отклоняется, функция закорачивается (прекращает выполнение любого кода в функции) и отклоняет асинхронное возвращенное обещание. Итак, вы получаете эту форму обработки ошибок бесплатно.

Затем, наконец, async функция также улавливает синхронные исключения для вас и превращает их в отклоненное обещание.

В обычной функции, которая возвращает обещание, такое как у нас было ранее:

 function run() {
    return getFromDatabase(someArg).then(key => {
        return getTheURL(key);
    }).then(url => {
        return getTheContent(url);
    }).then(content => {
         // some final processing
         return finalValue;
    });
}
 

Если getFromDatabase() генерируется синхронное исключение (возможно, вызванное из someArg -за недопустимости), то эта общая функция run() будет генерироваться синхронно. Это означает, что для вызывающего абонента, чтобы перехватить все возможные ошибки run() , они должны окружить его a try/catch , чтобы перехватить синхронные исключения, и использовать a .catch() , чтобы перехватить отклоненное обещание:

 try {
    run(someArg).then(finalValue => {
        console.log(finalValue);
    }).catch(err => {
        console.log(err);
    });
} catch(e) {
    console.log(err);
}
 

Это грязно и немного повторяющееся. Но, когда run() объявлено async , оно НИКОГДА не будет генерироваться синхронно, потому что любое синхронное исключение автоматически преобразуется в отклоненное обещание, поэтому вы можете быть уверены, что фиксируете все возможные ошибки, когда оно написано таким образом:

 async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}

// will catch all possible errors from run()
run(someArg).then(finalValue => {
    console.log(finalValue);
}).catch(err => {
    console.log(err);
});
 

Должен ли я всегда вызывать все свои функции с помощью await ?

Во-первых, вы бы использовали только await с функцией, которая возвращает обещание, поскольку await не предлагает никакой пользы, если функция не возвращает обещание (просто добавляя беспорядок в ваш код, если это не нужно).

Во-вторых, используете ли вы await или нет, зависит от контекста как вызывающей функции (поскольку вы ДОЛЖНЫ быть в async функции, чтобы использовать await , так и от потока логики и от того, выигрывает ли она от использования await или нет.

Места, где бессмысленно использовать await

 async function getKey(someArg) {
    let key = await getFromDatabase(someArg);
    return key;
}
 

await Здесь не делает ничего полезного. Вы не упорядочиваете несколько асинхронных операций и не выполняете никакой обработки возвращаемого значения. Вы можете выполнить точно такой же код, просто вернув обещание напрямую:

 async function getKey(someArg) {
    return getFromDatabase(someArg);
}
 

И, если вы знаете, что getFromDatabase() это никогда не выполняется синхронно, вы даже можете удалить async из объявления:

 function getKey(someArg) {
    return getFromDatabase(someArg);
}
 

Let’s say I’m writing a code composed of multiple functions within multiple files. If I end up using a library which returns a Promise or it’s an async function, should I trace back all my function calls from the asynchronous point to the entry point of the application and add an await before all the function calls after making them async?

This is a bit too general of an ask that’s hard to answer in a general case. Here are some thoughts along this general direction:

  1. Once any part of your result that you’re trying to return from your function A() is asynchronous or uses any asynchronous operation to obtain, the function itself is asynchronous. In plain Javascript, you can never return an asynchronous result synchronously so your function must use an asynchronous method to return the result (promise, callback, event, etc…).
  2. Any function B() that calls your asynchronous function A() that is also trying to return a result based on what it gets from A() is now also asynchronous and also must communicate its result back using an asynchronous mechanism. This is true for a function C() that calls B() and needs to communicate back its result to the caller. So, you can say that asynchronous behavior is infectious. Until you reach some point in the call chain where you no longer need to communicate back a result, everything has to use asynchronous mechanisms to communicate the result, error and completion.
  3. Нет особой необходимости отмечать функцию async , если вам конкретно не нужно одно из преимуществ async функции, такое как возможность использования await внутри этой функции или автоматическая обработка ошибок, которую она предоставляет. Вы можете писать функции, которые возвращают обещания просто отлично, не используя async объявление функции. Итак, «НЕТ», я не возвращаюсь к цепочке вызовов, делая все async . Я делаю функцию асинхронной только в том случае, если для этого есть конкретная причина. Обычно эта причина заключается в том, что я хочу использовать await внутри функции, но есть также автоматический перехват синхронных исключений, которые превращаются в отклонения обещаний, которые я описал ранее. Обычно это не требуется для хорошо управляемого кода, но иногда это полезно для плохо управляемого кода или кода с неопределенным поведением.
  4. await также используется только тогда, когда для этого есть конкретная причина. Я не просто автоматически использую его для каждой функции, которая возвращает обещание. Я описал выше причины ее использования. Все еще можно использовать .then() just fine для обработки результата от одного вызова функции, который возвращает обещание. В некоторых случаях это просто вопрос личного стиля, хотите ли вы использовать .then() или await , и нет особой причины, по которой это должно быть так или иначе.

Или, может быть, я должен просто привыкнуть вызывать все свои функции с помощью await независимо от того, являются ли они асинхронными или нет?

Абсолютно НЕТ! Во-первых, последнее, что вы хотите сделать, это взять идеально синхронный код и без необходимости сделать его асинхронным или даже заставить его выглядеть асинхронным. Асинхронный код (даже с async and await ) сложнее писать, отлаживать, понимать и поддерживать, чем синхронный код, поэтому вам никогда не захочется без необходимости превращать синхронный код в асинхронный, добавляя async/await в него:

Например, вы бы никогда этого не сделали:

 async function random(min, max) {
    let r = await Math.random();
    return Math.floor((r * (max - min))   min);
}
 

Во-первых, это совершенно синхронная операция, которая может быть закодирована следующим образом:

 function random(min, max) {
    let r = Math.random();
    return Math.floor((r * (max - min))   min);
}
 

Во-вторых, эта первая async реализация сильно усложнила использование функции, поскольку теперь она имеет асинхронный результат:

  random(1,10).then(r => {
     console.log(r);
 });
 

Вместо простого синхронного использования:

  console.log(random(1,10));
 

Комментарии:

1. Весь смысл моей load() функции заключался в том, чтобы выполнить какое-то асинхронное действие и сделать его синхронным с помощью await

2. @Mehran — Вы не можете сделать это в Javascript. Вы никогда не сможете взять асинхронно полученное значение и вернуть его синхронно. Async/await этого не делает. Ничто в Javascript этого не делает. Вы всегда получаете возвращенное обещание от async функции, и никакое использование await внутри этой функции этого не меняет. Пожалуйста, прочитайте подробности моего ответа, чтобы лучше понять, как async работает функция.

3. @Paulpro Но по-прежнему верно, что вы не можете сделать это на javascript — вам нужно сделать это на C (вы можете сделать это на node.js да, но с использованием API-интерфейсов C узла вместо API-интерфейсов javascript узла)

4. @Mehran — Хорошо, продолжайте и сделайте каждую отдельную функцию в вашей программе асинхронной, если хотите. Это усложнит ситуацию. Если вы не понимаете этого сейчас, вы поймете это лучше после того, как попробуете. Проверка на будущее не требуется. Синхронная функция не станет внезапно асинхронной в будущем, если вы не отредактируете ее так, чтобы она была такой, и если вы это сделаете, тогда вы измените дизайн, чтобы справиться с этим.

5. @Mehran — Кроме того, просто накладные расходы на создание обещания, его возврат, разворачивание цикла событий только для обработки await при каждом вызове отдельной функции, чтобы затем получить фактическое возвращаемое значение из обещания, безусловно, скажутся на производительности и сборке мусора по сравнению с обычным синхронным вызовом функции, не говоря уже о том, чтотеперь вам нужно await будет выполнять каждый вызов отдельной функции. Похоже, у вас серьезные проблемы с тем, как async/await работать в Javascript. Это то, что есть. Лучше всего научиться использовать ее так, как задумано, или перейти на другой язык, который вам больше нравится.

Ответ №2:

async / await — это просто синтаксический сахар, что означает, что они не привносят в язык никаких новых функций, являясь просто полезными оболочками для обещаний.

Если функция помечена как async , она всегда возвращает обещание:

 > async function f() { return 42; }
undefined
> f()
Promise { 42 }
 

Кроме того, если функция есть async , вы можете await выполнить любое обещание (включая результат другой async функции) внутри нее, и выполнение кода функции будет приостановлено до await тех пор, пока это обещание не будет разрешено или отклонено.

Чтобы ответить на ваш вопрос: если вы используете библиотечную функцию, вы обычно знаете, возвращает ли она обещание или нет (и если оно помечено как async , оно, безусловно, возвращает). Итак, убедитесь await , что для этого или используйте .then с возвращенным обещанием.

Комментарии:

1. Я не хочу показаться грубым, но как часто вы читаете код библиотеки, чтобы узнать, используется ли функция, которую вы используете async , или нет?

2. Я не согласен с тем, что async / await не привносят новые функциональные возможности в язык. Они позволяют вам писать асинхронный код гораздо более простыми способами, чем раньше, особенно при последовательности нескольких асинхронных операций. Хотя это правда, что вы можете кодировать что угодно без них, вы также можете кодировать что угодно без обещаний в целом, но обещания также привносят много новых функций в язык.

3. @Mehran — Все, что вам нужно сделать, это посмотреть на интерфейс функции и посмотреть, возвращает ли она обещание или нет. На самом деле вам все равно, возвращается ли обещание автоматически, потому что оно помечено как async функция, или обещание возвращается вручную. В любом случае интерфейс один и тот же — вызывающий имеет дело с возвращенным обещанием.

4. Я полностью согласен с этим await и async делаю ваш код намного более читабельным. Я хочу сказать, что они не такие, какими должны быть. На мой взгляд, они реализованы неправильно.

5. @Mehran — Ну, это только ваше мнение. Это очень полезные инструменты, которые могут значительно упростить асинхронное кодирование, чтение и обслуживание.

Ответ №3:

Потому что первая функция является асинхронной, поэтому она выполняется во время выполнения остальной части main функции, что бесполезно, когда результат регистрируется в следующей строке. Вам нужно дождаться завершения выполнения функции, прежде чем вы сможете использовать результат — так что либо используйте async / await как в вашем примере:

 async function main() {
  const data = await load();
  console.log(`Loaded data: ${JSON.stringify(data)}`);
}
 

Или используйте .then :

 function main() {
  load().then(data => {
    console.log(`Loaded data: ${JSON.stringify(data)}`);
  });
}
 

Подсказка здесь такова: если функция есть async , вы должны использовать ее async в хронологическом порядке, потому что она всегда возвращает обещание.

Комментарии:

1. В Javascript, когда обещание немедленно разрешается, обратный вызов выполняется только тогда, когда стек вызовов свободен. Что происходит в c #, когда обещание немедленно разрешается?