HTML / CSSJavaScriptNode jsПаттерны проектированияПрактические

Callback hell ("The Pyramid of Doom")

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

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

1function onGetUserInfo(id, handler) {
2 setTimeout(() => {
3 const userData = {
4 id: 1,
5 name: "test"
6 }
7 handler(userData);
8 },
9 100);
10}
11
12function onGetPermissions(userData, handler) {
13 setTimeout(() => {
14 const permissions = {
15 write: "y",
16 read: "y"
17 }
18 handler(permissions, userData);
19 },
20 50);
21}
22
23function makePost(permissions, userData) {
24 if (permissions.write === "y") {
25 console.log(`Пользователь ${userData.name} написал пост`);
26 }
27}
28
29
30// реализация паттерна Callback Hell
31onGetUserInfo(1, (userData) => {
32 onGetPermissions(userData, (permissions, userData) => {
33 makePost(permissions, userData); // будет выведено "Пользователь test написал пост"
34 });
35});

Можно заметить, что паттерн "Callback Hell" не удобен для восприятия, из-за этого он и получил свое название. Чем больше уровней вложенности коллбеков, тем труднее понять что происходит.

Еще пример:

1const verifyUser = function(username, password, callback){
2 dataBase.verifyUser(username, password, (error, userInfo) => {
3 if (error) {
4 callback(error)
5 }else{
6 dataBase.getRoles(username, (error, roles) => {
7 if (error){
8 callback(error)
9 }else {
10 dataBase.logAccess(username, (error) => {
11 if (error){
12 callback(error);
13 }else{
14 callback(null, userInfo, roles);
15 }
16 })
17 }
18 })
19 }
20 })
21};

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

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

Этот пример становится еще более сложным, если вы понимаете, что database.getRoles - это еще одна функция, которая имеет вложенные обратные вызовы.

1const getRoles = function (username, callback){
2 database.connect((connection) => {
3 connection.query('get roles sql', (result) => {
4 callback(null, result);
5 })
6 });
7};

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

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

При наличии промисов код в нашем примере с асинхронным JavaScript будет выглядеть примерно так:

1const verifyUser = function(username, password) {
2 database.verifyUser(username, password)
3 .then(userInfo => dataBase.getRoles(userInfo))
4 .then(rolesInfo => dataBase.logAccess(rolesInfo))
5 .then(finalResult => {
6 //do whatever the 'callback' would do
7 })
8 .catch((err) => {
9 //do whatever the error handler needs
10 });
11};

Чтобы добиться такой простоты, все функции, используемые в примере, должны быть "промисифицированы". Давайте посмотрим, как будет обновлен метод getRoles, чтобы он возвращал обещание:

1const getRoles = function (username){
2 return new Promise((resolve, reject) => {
3 database.connect((connection) => {
4 connection.query('get roles sql', (result) => {
5 resolve(result);
6 })
7 });
8 });
9};

Мы изменили метод так, чтобы он возвращал Promise с двумя обратными вызовами, а сам promise выполняет действия из метода. Теперь обратные вызовы resolve и rejec будут сопоставлены с методами Promise.then и Promise.catch соответственно.

Вы можете заметить, что метод getRoles все еще внутренне подвержен феномену "the pyramid of doom". Это связано с тем, как создаются методы базы данных, поскольку они не возвращают Promise. Если бы наши методы доступа к базе данных также возвращали Promise, метод getRoles выглядел бы следующим образом:

1function getRoles () {
2 return new Promise((resolve, reject) => {
3 database.connect()
4 .then((connection) => connection.query('get roles sql'))
5 .then((result) => resolve(result))
6 .catch(reject)
7 });
8};

Promise hell

"The pyramid of doom" была значительно смягчена с появлением промисов. Однако нам по-прежнему приходилось полагаться на обратные вызовы, которые передаются в методы .then и .catch Promise. Что могло привести к Promise Hell.

Promise Hell Может выглядеть так:

1connectToDataBase().then( database => {
2 return getUsers(database).then(users =>{
3 return getUserSettings(database).then( settings =>{
4 return enableAccess(settings,users)
5 })
6 })
7})

Или так:

1getListOfItems()
2 .then(items => {
3 async.map(
4 items,
5 (item, it) => {
6 dbModels.someFunction(item)
7 .then(elements => {
8 async.map(
9 elements,
10 (element, it) => {
11 dbModels.someFunction(someValue)
12 .then(someValue => {
13 dbModels.someFunction(someValue)
14 .then(someValue => {
15 async.map(
16 someValue,
17 (someValue, someValue) => {
18 dbModels.someFunction(someValue)
19 .then(someValue => {
20 dbModels.someFunction(someValue, "active")
21 .then(someValue => {
22 dbModels.someFunction(someValue)
23 .then(someValue => {
24 dbModels.someFunction(someValue)
25 .then(someValue => {
26 const config = {
27 };
28 return someCallbackFunction(null, config);
29 });
30 });
31 });
32 })
33 },
34 (error, data) => {
35 const someObject = Object.assign({}, someKey);
36 someObject.data = data;
37 return it(null, someObject);
38 }
39 );
40 });
41 }).catch(error => {
42 console.error("Could not load the setting", error);
43 })
44 },
45 (error, listOfItems) => {
46 return it(error, listOfItems);
47 }
48 );
49 });
50 }, (err, listOfItems) => {
51 return callback(err, _.flatten(listOfItems));
52 });
53 }).catch(error => {
54 console.error("Failed to load items", error);
55 });

Promises открыли путь к одному из самых крутых улучшений в JavaScript. ECMAScript 2017 привнес синтаксический сахар поверх обещаний в JavaScript в форме операторов async и await. Они позволяют нам писать код на основе Promise, как если бы он был синхронным, но без блокировки основного потока.

Async await

Чтобы использовать async/await, нужна функция, возвращающая Promise Эта функция должна иметь префикс async, прежде чем она сможет использовать await.

Функция async/await может выглядеть так:

1async function asyncCall() {
2 let result = await fetchAddByTwoPromise(n);
3 result = await fetchAddByTwoPromise(result);
4 return await fetchAddByTwoPromise(result);
5}

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

Чтобы получить результат, мы можем вызвать функцию async и проверить возвращенное обещание:

1asyncAwaitExample(2).then((r) => console.log(r));

Вспомните код выше который был написан с помощью коллбеков с помощью async await он может выглядеть следующим образом:

1const verifyUser = async function(username, password){
2 try {
3 const userInfo = await dataBase.verifyUser(username, password);
4 const rolesInfo = await dataBase.getRoles(userInfo);
5 const logStatus = await dataBase.logAccess(userInfo);
6 return userInfo;
7 }catch (e){
8 //handle errors as needed
9 }
10};

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

Другие примеры с async/await

1function roomCleaning(){
2 return new Promise(resolve,reject=>{
3 const num = Math.random();
4 if (num >= 0.5) {
5 resolve(num);
6 } else {
7 reject(new Error(num));
8 }
9})
10}
11
12
13async function myAsyncFunc() {
14 try{
15 let result = await roomCleaning();
16 console.log(result);
17 }
18 catch(err) {
19 console.log(err);
20 }
21}
22myAsyncFunc();
ℹ️На заметку:
  • ключевое слово async используется перед функцией. Это означает, что функция всегда возвращает Promise, даже если оно не возвращает в нем обещание.
  • Ключевое слово await используется перед вызовом функции Promise, оно заставляет JavaScript ждать, пока обещание не будет выполнено, и возвращает свой результат.
  • Ошибка обрабатывается обычным методом try… catch. В случае отклонения обещания он переходит к блоку catch.
  • Ключевое слово await может использоваться только внутри функций, определенных с помощью async, что означает, что мы не можем использовать await на верхнем уровне нашего кода, поскольку это не внутри функции async.

Практика 👨‍💻👨‍💻

Используя задания из практики Promises и Promise.all() & Promise.race() переписать функции используя async/await синтаксис.

Hello