Статьи

Обратные вызовы JavaScript, обещания и асинхронные функции: часть 1

Существует много разговоров об асинхронном программировании, но что именно в этом такого? Дело в том, что мы хотим, чтобы наш код был неблокирующим.

Задачи, которые могут блокировать наше приложение, включают выполнение HTTP-запросов, запросы к базе данных или открытие файла. Некоторые языки, такие как Java, решают эту проблему, создавая несколько потоков. Однако в JavaScript есть только один поток, поэтому нам нужно спроектировать наши программы так, чтобы никакая задача не блокировала поток.

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

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

  • Потоки
  • Синхронный и Асинхронный
  • Функции обратного вызова
  • Резюме
  • Ресурсы

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

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

Приложения также могут быть параллельными. Это достигается с помощью потоков. Я не буду слишком углубляться, потому что это выходит за рамки этой статьи. Если вы хотите получить подробное объяснение того, как JavaScript работает скрытно, я рекомендую посмотреть это видео .

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

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

Это похоже на то, как работает асинхронное программирование. Кассир мог бы подождать меня. Иногда они делают. Но лучше не задерживаться в очереди и не проверять других клиентов. Дело в том, что клиенты не должны быть выписаны в том порядке, в котором они находятся в очереди. Аналогично, код не должен выполняться в том порядке, в котором мы его пишем.

Естественно думать, что наш код выполняется последовательно сверху вниз. Это синхронно. Однако в JavaScript некоторые задачи изначально асинхронны (например, setTimeout), а некоторые задачи мы проектируем как асинхронные, потому что заранее знаем, что они могут блокироваться.

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

Это синхронный способ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require(‘fs’);
const path = require(‘path’);
const postsUrl = path.join(__dirname, ‘db/posts.json’);
const commentsUrl = path.join(__dirname, ‘db/comments.json’);
 
//return the data from our file
function loadCollection(url) {
    try {
        const response = fs.readFileSync(url, ‘utf8’);
        return JSON.parse(response);
    } catch (error) {
        console.log(error);
    }
}
 
//return an object by id
function getRecord(collection, id) {
    return collection.find(function(element){
        return element.id == id;
    });
}
 
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
    return comments.filter(function(comment){
        return comment.postId == postId;
    });
}
 
//initialization code
const posts = loadCollection(postsUrl);
const post = getRecord(posts, «001»);
const comments = loadCollection(commentsUrl);
const postComments = getCommentsByPost(comments, post.id);
 
console.log(post);
console.log(postComments);
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
[
    {
        «id»: «001»,
        «title»: «Greeting»,
        «text»: «Hello World»,
        «author»: «Jane Doe»
    },
    {
        «id»: «002»,
        «title»: «JavaScript 101»,
        «text»: «The fundamentals of programming.»,
        «author»: «Alberta Williams»
    },
    {
        «id»: «003»,
        «title»: «Async Programming»,
        «text»: «Callbacks, Promises and Async/Await.»,
        «author»: «Alberta Williams»
    }
]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
[
    {
        «id»: «phx732»,
        «postId»: «003»,
        «text»: «I don’t get this callback stuff.»
    },
    {
        «id»: «avj9438»,
        «postId»: «003»,
        «text»: «This is really useful info.»
    },
    {
        «id»: «gnk368»,
        «postId»: «001»,
        «text»: «This is a test comment.»
    }
]

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

В Node есть метод readFile который мы можем использовать для асинхронного открытия файла. Это синтаксис:

1
2
3
fs.readFile(url, ‘utf8’, function(error, data) {
    …
});

Мы можем испытывать желание вернуть наши данные внутри этой функции обратного вызова, но мы не сможем использовать их внутри нашей функции loadCollection . Наш код инициализации также нужно будет изменить, потому что у нас не будет правильных значений, которые нужно назначить нашим переменным.

Чтобы проиллюстрировать проблему, давайте рассмотрим более простой пример. Как вы думаете, следующий код будет печатать?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
function task1() {
    setTimeout(function() {
        console.log(‘first’);
    }, 0);
}
 
function task2() {
    console.log(‘second’);
}
 
function task3() {
    console.log(‘third’);
}
 
task1();
task2();
task3();

В этом примере будет напечатано «второе», «третье», а затем «первое». Неважно, что функция setTimeout имеет задержку 0. Это асинхронная задача в JavaScript, поэтому она всегда будет отложена для выполнения позже. Функция firstTask может представлять любую асинхронную задачу, например, открытие файла или запрос к нашей базе данных.

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

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

Обратные вызовы позволяют принудительно выполнять задачи последовательно. Они также помогают нам, когда у нас есть задачи, которые зависят от результатов предыдущей задачи. Используя обратные вызовы, мы можем исправить наш последний пример, чтобы он печатал «first», «second», а затем «третий».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function first(cb) {
    setTimeout(function() {
        return cb(‘first’);
    }, 0);
}
 
function second(cb) {
    return cb(‘second’);
}
 
function third(cb) {
    return cb(‘third’);
}
 
first(function(result1) {
    console.log(result1);
    second(function(result2) {
        console.log(result2);
        third(function(result3) {
            console.log(result3);
        });
    });
});

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

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

1
2
3
4
5
6
7
8
9
function loadCollection(url, callback) {
    fs.readFile(url, ‘utf8’, function(error, data) {
        if (error) {
            console.log(error);
        } else {
            return callback(JSON.parse(data));
        }
    });
}

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

1
2
3
4
5
6
7
8
9
loadCollection(postsUrl, function(posts){
    loadCollection(commentsUrl, function(comments){
        getRecord(posts, «001», function(post){
            const postComments = getCommentsByPost(comments, post.id);
            console.log(post);
            console.log(postComments);
        });
    });
});

В нашей функции loadCollection следует заметить, что вместо использования оператора try/catch для обработки ошибок мы используем оператор if/else . Блок catch не сможет перехватить ошибки, возвращаемые readFile вызовом readFile .

Хорошей практикой является наличие в нашем коде обработчиков ошибок для ошибок, которые являются результатом внешних воздействий, а не ошибок программирования. Это включает в себя доступ к файлам, подключение к базе данных или выполнение HTTP-запроса.

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

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

Для удобства пользователей было бы возвращать пользователю сообщение об ошибке и позволить ему повторить попытку входа. В нашем примере файла мы могли бы передать объект ошибки в функцию обратного вызова. Это имеет место с функцией readFile . Затем, когда мы выполняем код, мы можем добавить оператор if/else для обработки успешного результата и отклоненного результата.

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

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

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