Howtojs

Как вернуть результат из асинхронного вызова?

2 февраля 2021 г. • ☕️ 4 мин.

Проблема

Такой код часто вводит в заблуждение разработчиков, не знакомых с понятием асинхронности в языке JavaScript.

const fs = require("fs")

let file

fs.readFile("./file.txt", (err, result) => {
  if (err) throw err
  file = result
  return result
})

console.log(file) // undefined - why?

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

В JavaScript есть возможность писать, как синхронный так и асинхронный код. Что это значит и в чем разница?

I/O

Когда мы хотим взаимодействовать с внешним окружением из нашей программы (прочитать файл с диска, запросить информацию по сети), мы используем понятие Ввод/Вывод (Input/Output) или I/O.

I/O вызов может быть блокирующим (blocking) или не блокирующим (non-blocking).

Блокирующий вызов

Блокирующий вызов останавливает (блокирует) выполнение программы, до тех пор, пока мы не получим результат.

const fs = require("fs")

// программа ожидает, пока мы не прочитаем файл целиком
const file = fs.readFileSync("./file.txt")
console.log(file)

Не блокирующий вызов

Не блокирующий вызов продолжает выполнение программы, не дожидаясь результата.

const fs = require("fs")

// программа продолжает выполнение
fs.readFile("./file.txt", (err, file) => {  if (err) throw err
  console.log(file)
})

// ... more code

Решение

Как получить резлуьтат из не блокирующего вызова если программа продолжила выполнять другой код не дожидаясь результата? Попросить окружение вернуть нам результат, когда он будет готов.

Для этого мы передаем функцию обратного вызова Callback в не блокирующий вызов, например readFile.

Что значит попросить окружение? Окружение это та среда, где мы запускаем программу, например Browser или Node.js на сервере.

fs.readFile("./file.txt", callback) // (1)
function callback(err, file) {
  if (err) throw err
  console.log(file)
}

(1) Дословно, здесь мы говорим: Эй, Node.js, прочитай файл с именем "./file.txt" и как будешь готов, вызови функцию callback с результатом.

Последовательность действий

Как сделать несколько вызовов, один за другим?

Есть несколько решений:

Вложить один вызов, в callback другого

const fs = require("fs")

fs.readFile("./file-with-cat.txt", (err, fileWithCat) => {  if (err) throw err

  fs.readFile("./file-with-dog.txt", (err, fileWithDog) => {    if (err) throw err

    console.log(fileWithCat.length + fileWithDog.length)    // more code...
  })
})

Использовать Promise

const fs = require('fs/promises')

fs.readFile("./file-with-cat.txt")  .then(fileWithCat => {
    console.log(fileWithCat)
    return fs.readFile("./file-with-dog.txt")  })
  .then((fileWithDog) => {
    console.log(fileWithCat)  })

Использовать async/await

const fs = require('fs/promises')

async function main() {
  const fileWithCat = await fs.readFile("./file-with-cat.txt")  const fileWithDog = await fs.readFile("./file-with-dog.txt")}

main()

Заключение

Теперь ты должен понимать, как правильно писать и читать код с не блокирующими вызовами I/O.