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}1112function onGetPermissions(userData, handler) {13 setTimeout(() => {14 const permissions = {15 write: "y",16 read: "y"17 }18 handler(permissions, userData);19 },20 50);21}2223function makePost(permissions, userData) {24 if (permissions.write === "y") {25 console.log(`Пользователь ${userData.name} написал пост`);26 }27}282930// реализация паттерна Callback Hell31onGetUserInfo(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 do7 })8 .catch((err) => {9 //do whatever the error handler needs10 });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
соответственно.
Вы можете заметить, что метод getRole
s все еще внутренне подвержен феномену "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 needed9 }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}111213async 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 синтаксис.