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

«Обычная» программа vs неблокирующие функции

В первый день обучения программированию нас обычно учат, что программы выполняются последовательно. Рассмотрим следующую программу:

function main() {
    console.log("1");
    console.log("2");
    console.log("3");
}

main();

---

Output:
1
2
3

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

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

function main() {
    console.log('1')
    setTimeout(function () {
        console.log('2'), 1000
    })
    console.log('3');
}

main();

---
Output: 
1
3
2

В выводе показано, что 3 напечатано раньше 2. Это связано с тем, что функция setTimeout неблокируется, а это означает, что при ее вызове программа немедленно запускает следующую строку console.log('3')не дожидаясь, пока setTimeout завершится первым.

Но какой смысл в неблокирующих функциях?

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

function downloadFile(filename) {
    setTimeout(function () {
        console.log(filename + " downloaded");
    }, 1000)
}

function main() {
    downloadFile("file1")
    downloadFile("file2")
    downloadFile("file3")
    downloadFile("file4")
}

main();

---
Output:
file1 downloaded
file2 downloaded
file3 downloaded
file4 downloaded
---
Time taken: 1.057s

Функция downloadFile представляет процесс загрузки и занимает 1 секунду. Теперь, несмотря на то, что я вызвал downloadFile 4 раза, общее время, затраченное на завершение всех 4 загрузок, составляет 1,057 секунды. На высоком уровне вы можете представить, что все 4 функции downloadFile выполняются «одновременно».

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

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

P.S. В Linux/Mac, чтобы рассчитать время выполнения программы от начала до конца, вы можете запустить time node index.js в своем терминале.

Так что же такое обещания?

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

  1. Войти в систему
  2. Получить сообщения пользователя

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

function login() {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve("User logged in")
        }, 2000)
    })
}

function getUserPosts() {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve("Got user's posts")
        }, 1000)
    })
}

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

Чтобы подробнее рассказать о Promise(), он принимает функцию обратного вызова с двумя аргументами: resolve и reject. При успешном завершении функции мы передаем результат, который хотим вернуть, в resolve(),, а в случае сбоя мы передаем ошибку в ignore(). В этом уроке мы не будем использовать ignore, чтобы не усложнять ситуацию, но это просто механизм обработки ошибок.

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

Теперь давайте посмотрим, как использовать промисы для запуска наших функций в правильном порядке:

async function main() {
    login().then(function (res1) {
        console.log(res1)
        getUserPosts().then(function (res2) {
            console.log(res2)
        })
    })
}

main();

---
Output:
User logged in
Got user's posts

Когда функция (назовем эту функцию 1) возвращает обещание, она автоматически будет иметь метод .then(), который принимает функцию обратного вызова (назовем эту функцию 2) будет выполнен после завершения функции 1. Кроме того, функция 2 принимает один аргумент, который выше мы назвали res1, и это всего лишь разрешение функции 1. Разрешение функции – это просто значение, которое мы хотим вернуть внутри промиса, когда функция завершает выполнение. Вспомните, как мы передали «Пользователь вошел в систему» и «Получены сообщения пользователя» в resolve() в нашем функции выше. Это то, что представляют собой res1 и res2 соответственно.

В результате использования промисов наша функция всегда будет выполняться в правильном порядке: login() вызывается и выполняется до getUserPosts(). .

Что такое async/await?

Если вы разобрались в промисах, то изучить async/await будет несложно, поэтому убедитесь, что вы полностью понимаете, как работают промисы, прежде чем продолжить изучение этого раздела!

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

async function main() {
    let res1 = await login();
    console.log(res1);

    let res2 = await getUserPosts();
    console.log(res2);
}

main();

Чтобы объяснить, как работает асинхронное ожидание:

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

Так зачем использовать асинхронное ожидание? Что ж, эта функция особенно полезна, если у вас есть несколько функций-промисов, потому что при использовании синтаксиса promise.then() вы получаете большое количество вложений, что снижает читаемость кода. Тогда как при использовании async await все аккуратно записывается в одном и том же отступе и, следовательно, гораздо более читабельно для человеческого глаза.

Примечание. Разница между async/await и Promises.

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

import fs from 'fs'

function writeToFile(filePath, contents) {
    fs.writeFile(filePath, contents, (err) => {
        if (err) {
            console.log("Failed to write to file");
        } else {
            console.log("Successfully written to file");
        }
    })
}

async function main() {
    await writeToFile("your_file.txt", "Hello World 1")
    await writeToFile("your_file.txt", "Hello World 2")
}

main();

Здесь у нас есть функция, которая записывает в файл и принимает функцию обратного вызова, когда запись завершена. Однако, поскольку fs.writeFile() является неблокирующей функцией, если бы мы вызвали writeToFile дважды, как показано в main, два вызова будут выполняться «одновременно», и ключевое слово await не будет иметь никакого эффекта. В результате вы не будете знать, будет ли «Hello World 1» или «Hello World 2» последней строкой, записанной в ваш файл.

Если мы хотим, чтобы эта программа выполнялась по порядку, мы можем использовать Promises, что показано ниже:

import fs from 'fs'

function writeToFile(filePath, contents) {
    return new Promise(function (res, rej) {
        fs.writeFile(filePath, contents, (err) => {
            if (err) {
                console.log("Failed to write to file");
            } else {
                console.log("Successfully written to file");
            }
        })
    })
}

async function main() {
    await writeToFile("your_file.txt", "Hello World 1")
    await writeToFile("your_file.txt", "Hello World 2")
}

main();

Теперь «Hello World 1» всегда будет записываться в файл первым, а «Hello World 2» всегда будет записываться вторым. Будем надеяться, что этот пример успешно демонстрирует, что неблокирующим функциям не обязательно возвращать обещание, и чтобы добиться такого поведения, иногда приходится создавать собственное обещание.

Заключительные мысли

Надеюсь, эта статья развеяла все ваши сомнения по поводу промисов и синтаксиса async/await в JS. Если нет, оставьте комментарий ниже, и я обязательно постараюсь вам помочь!

Кстати, если вы создаете NodeJS-сервер и хотите немедленно применить на практике недавно изученные концепции (например, обещания!), не беспокоясь о синтаксических ошибках при написании шаблонного кода, взгляните на Visual Backend (https:/ /visual-backend.com). Он автоматически генерирует шаблонный код и управляет структурой вашего проекта, чтобы вы могли просто сосредоточиться на написании основных функций вашего приложения.

Благодарим вас за прочтение этой статьи. Если у вас возникнут вопросы, вы всегда можете связаться со мной по адресу [email protected]. С нетерпением ждем встречи с вами здесь, в следующей статье. Ваше здоровье!