Що таке Callback функція в JS?
Callback функцію можна просто визначити як функцію, яка використовується як параметр в іншій функції.
Тож чому ми використовуємо callback функції?
У JavaScript callback функції використовуються, коли ви хочете виконати код послідовно. Загальний приклад — це використання прослуховувачів подій (event listeners), де часто застосовуються callback функції.
Ось приклад:
document.getElementById('myButton').addEventListener('click', function() {
console.log('Button was clicked!');
});
Код вище означає:
“Виконайте цей код, коли кнопка буде натиснута.”
Подивившись на структуру addEventListener(‘click’, function() {}), ми можемо побачити, що це функція, і function(), яка використовується як параметр у прослуховувачі подій (event listener), є callback функцією.
Ось ще один приклад:
setTimeout (function(){
//код для виконання}, 1000)
Функція setTimeout() означає:
“Будь ласка, виконайте цей код через 1 секунду.”
Перший аргумент — це callback функція, яка буде виконана після вказаної затримки (у цьому випадку 1000 мілісекунд = 1 секунда).
Ось дві характеристики callback функцій:
- Callback функцію також можна оголосити поза функцією і передати її для використання.
function callbackFunction(){}
document.querySelector('.button').addEventListener('click', callbackFunction)
- Callback функція може отримати власне ім’я.
setTimeout (function Hello(){
//код для виконання}, 1000)
- Callback функція може використовуватись тільки в тій функції, якій потрібна callback функція. [наприклад, addEventListener( )]
Принципи Callback функцій
Чому ми використовуємо Callback функції?
Вищезазначений код насправді можна переписати, як показано нижче, і він дасть той самий результат:
Однак уявімо, що в спільному проєкті перша функція, яку я створив, часто використовується.
• A хоче виконати console.log(2) після виклику first().
• B хоче виконати console.log(4) після виклику first().
У сценарії асинхронної обробки просто виконання first() не гарантує, що наступний рядок коду завжди буде виконано відразу після нього. Це тому, що асинхронні функції не блокують виконання коду, а продовжують виконувати інші завдання, а вказаний код виконується лише після завершення асинхронної операції.
Тому, щоб забезпечити послідовне виконання коду, як цього хочуть A та B, давайте просто оновимо функцію first() і використаємо callback функцію, як показано в коді нижче:
Однак пам’ятайте про недоліки, коли використовується забагато callback функцій.
Наприклад, коли намагаєтесь послідовно отримувати дані з бази даних — спочатку A, потім B і, зрештою, C — код може стати занадто довгим і вкладеним, утворюючи таку ситуацію:
Як ви можете побачити, код стає все більш вкладеним праворуч, що ускладнює його читання, підтримку та відлагодження.
Це класичний приклад callback hell.
Рішення: Використання Promises або Async/Await для уникнення callback hell
Для покращення читабельності коду та чистішої обробки асинхронних операцій ми можемо використовувати Promise або async/await, що є частиною асинхронного програмування (Asynchronous Programming).
Отже, що таке асинхронне програмування в JS?
Ось приклад:
Поки їхня мама відсутня, два брати планують розподілити домашні справи.
Однак брати мали різні підходи до виконання своїх завдань. Старший брат чекав, поки пральна машина закінчить роботу, а молодший брат тим часом запустив посудомийну машину, підготував інгредієнти для вечері, пропилососив будинок і закінчив миття підлоги. Це можна показати, як у таблиці нижче:
Молодшого брата дратувало, що старший брат нічого не робить, просто чекаючи, поки пральна машина закінчить роботу.
Тут підхід старшого брата можна розглядати як синхронний, а підхід молодшого брата — як асинхронний.
У чому різниця між синхронним і асинхронним виконанням?
Давайте розглянемо різницю між синхронним та асинхронним виконанням на прикладі коду JS.
Синхронний
public void syncBlackBean() {
bowlStatus="BlackBean searved in a Bowl"
BlackBean_Delivery();
BlackBean_Eating_Sync();
next_destination();
}
public void BlackBean_Delivery(){
System.out.println("Bowl Status upon delivery: " + bowlStatus);
}
public void BlackBean_Eating_sync(){
plateStatus = restTemplate
.getForObject(
"http://localhost:3000/eat-noodle-2sec",
String.class
);
System.out.println("Bowl Status after eating: " + bowlStatus);
}
public void nextDeliverySpot(){
System.out.println("Bowl Status while moving: " + bowlStatus);
};
Функція syncBlackBean()
виконує наступні три функції в порядку, в якому код написаний, так само як старший брат виконував домашні справи:
- BlackBean_Delivery();
- BlackBeanEatingSync();
- next_destination();
Асинхронний
public void asyncBlackBean() {
bowlStatus="BlackBean searved in a Bowl"
BlackBean_Delivery();
BlackBean_Eating_Sync();
next_destination();
}
public void BlackBean_Delivery(){
System.out.println("Bowl Status upon delivery: " + bowlStatus);
}
public void BlackBean_Eating_async(){
ListenableFuture> entity
= asyncRestTemplate
.getForEntity(
"http://localhost:3000/eat-noodle-2sec",
String.class);
entity.addCallback(
result -> {
plateStatus = result.getBody();
System.out.println("Bowl Status after eating: " + bowlStatus);
}, e -> e.printStackTrace());
}
public void nextDeliverySpot(){
System.out.println("Bowl Status while moving: " + bowlStatus);
};
Коли функція asyncBlackBean()
виконується:
-
Спочатку BlackBean доставляється в мисці.
-
Без затримки виконується
nextDeliverySpot()
. -
Миска все ще містить BlackBean на цей момент.
-
Через 2 секунди, коли клієнт завершить їсти, відображається статус порожньої миски.
Іншими словами, порядок виконання функцій такий:
-
BlackBean_Delivery();
-
next_destination();
-
BlackBeanEatingasync();
Це означає, що асинхронні функції не виконуються рядок за рядком у порядку, в якому вони з’являються згори вниз.
Чому ми використовуємо асинхронні функції?
Давайте подумаємо про синхронну функцію.
Якщо кур'єр повинен чекати, поки клієнт не закінчить їсти страву, а потім переходити до наступного пункту доставки, виникне втрата часу, тому що поїдання локшини з чорними бобами — це робота клієнта, а не кур'єра.
Тому, коли кур'єр передає локшину з чорними бобами клієнту, просто скажіть йому, що він повинен зробити після того, як клієнт завершить їсти, що написано як results->{} в коді вище, і він може перейти до наступного пункту доставки.
Ми можемо сказати, що це різні потоки, клієнт, що їсть локшину, і кур'єр, що доставляє їжу, працюють в різних потоках.
Тому, коли програма асинхронна, це означає, що є кілька потоків або процесів, які працюють одночасно, як багатозадачність.
function asyncBlackBeanTimer (seconds) {
console.log("BlackBean Delivered");
setTimeout(
function () {
console.log("Finished Eating");
}, seconds * 1000
);
console.log("Delivery man left");
}
//setTimeout() таймер для встановлення
asyncBlackBeanTimer(1);
Результат буде показано так:
- BlackBean Delivered
- Delivery man left
- Finished Eating // (через 1 секунду)
JavaScript працює на JavaScript-движку у веб-браузері або Node.js,
У цьому движку є один потік для виконання JavaScript і ще один потік для Web API, який виконує ресурсномісткі завдання, такі як використання таймерів, надсилання HTTP-запитів за допомогою AJAX або читання даних з файлів.
Коли код JavaScript виконується, асинхронні завдання, які зазвичай мають прикріплені callback функції, спочатку надсилаються на джерело асинхронної обробки. Як тільки завдання виконуються, вони надсилають свої callback функції в чергу завдань. Ці callback функції виконуються по черзі, але тільки після того, як весь синхронний код JavaScript завершить своє виконання. Це те, що ми називаємо event loop.
Однак, якщо callback функції вкладені кілька разів, це може призвести до ‘Callback hell’, де код стає менш читабельним, зростає ризик помилок, а налагодження також стає дуже складним.
Розглянемо приклад нижче:
Ось код для пошуку вчителя старшої школи студента в університеті.
function search_studentInfo (studentID, toDo_with_studentInfo) {
ajax(
baseUrl + “student-info/“ + studentID,
function (response) {
toDo_with_studentInfo (response);
}
);
}
function search_highschool_DB_address (highschool_name, toDo_with_address) {
ajax(
baseUrl + “highschool-db/“ + highschool_name,
function (response) {
toDo_with_address (response);
}
);
}
function search_highschool_course (highschool_DB_address, student_registrationNumber, toDo_with_classList) {
ajax(
baseUrl + “classes/“ + highschool_DB_address +”/“+ student_registrationNumber,
function (response) {
toDo_with_classList (response);
}
);
}
function search_courseInfo (studentYear3_math_classCode, toDo_with_courseInfo) {
ajax(
baseUrl + “class-info/“ + studentYear3_math_classCode,
function (response) {
toDo_with_courseInfo (response);
}
);
}
function search_studentYear3_mathTeacher (studentID) {
search_studentInfo (studentID,
function (studentInfo) {
let student_registrationNumber= studentInfo [‘Registration Number’];
let highschool_name = studentInfo [‘High School Name’];
search_highschool_DB_address (highschool_name,
function (highschool_DB_address) {
search_highschool_course (highschool_DB_address, student_registrationNumber,
function (highschool_course) {
let studentYear3_math_classCode = classList [“Year3_math”];
search_courseInfo (studentYear3_math_classCode,
function (courseInfo) {
console.log(`Teacher: ${ courseInfo [“teacherName”]}`);
}
)
}
)
}
)
}
)
}
search_studentYear3_mathTeacher ('12345');
Коли сервер школи отримує studentID, викликається функція `search_studentInfo` з callback функцією [function (studentInfo)], яка вказує, що робити з отриманою інформацією як аргумент.
2. Callback функція з кроку 1 витягує **реєстраційний номер студента** і **назву школи**, і використовує ці дані для пошуку адреси бази даних відповідної школи.
3. Адреса бази даних передається іншій callback функції [function (highschool_DB_address)], яка отримує **курси школи**, які відвідував студент, використовуючи його реєстраційний номер.
4. Через цю callback функцію список курсів шукається за ключовим словом Year3_math. Як тільки курс знаходиться, викликається функція `search_courseInfo()`, щоб отримати інформацію про курс.
5. Нарешті, остання callback функція витягує **teacherName** з інформації про курс і виводить її.
Цей тип коду називається **“callback hell”**. Навіть у такому простому прикладі код стає занадто складним, що робить його непридатним для використання в реальному світі.
## **Promise для запобігання Callback Hell**
function searchstudentInfoPromise (studentID) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "student-info/" + studentID,
function (response) {
resolve(response);
});
}
function searchhighschoolDBaddressPromise (studentregistrationNumber, highschoolname) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "highschool-db/" + highschoolname,
function (response) {
resolve ([studentregistrationNumber, response]);
});
})
}
function searchhighschoolcoursePromise (studentregistrationNumber,highschoolDBaddress) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "classes/" + highschoolDBaddress + "/" + student_registrationNumber,
function (response) {
resolve(response);
});
});
}
function searchcourseInfoPromise (studentYear3mathclassCode) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "class-info/" + studentYear3mathclassCode,
function (response) {
resolve(response);
});
});
}
```
search_studentInfo_Promise ("12345")
.then(function (studentInfo) {
let student_registrationNumber = studentInfo['Registration Number'];
let highschool_name = studentInfo['highschool_name'];
return search_highschool_DB_address_Promise (student_registrationNumber, highschool_name)
})
.then(function (student_registrationNumber AND highschool_DB_address) {
return search_highschool_course_Promise(
student_registrationNumber AND highschool_DB_address[0],
student_registrationNumber AND highschool_DB_address[1]
)
})
function search_highschool_DB_address_Promise (student_registrationNumber, highschool_name) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "highschool-db/" + highschool_name,
function (response) {
resolve ([student_registrationNumber, response]);
});
})
}
.then(function (classList) {
let studentYear3_math_classCode = classList["Year3 math"];
return search_courseInfo_Promise (studentYear3_math_classCode);
})
.then(function (classInfo) {
console.log(`Teacher: ${ courseInfo["teacherName"]}`);
});
У коді вище асинхронні функції повертають об'єкти Promise. Ці Promises приймають конструктивну функцію з:
- першим аргументом [ajax(baseUrl + …)], що виконує асинхронне завдання.
- другим аргументом [function(response) { resolve(response); })], який передає результат в callback функцію.
- Функція [function searchstudentInfoPromise("12345")] використовується для отримання studentInfo.
• Ця функція повертає Promise і використовує .then()
, щоб підключити callback для наступної операції.
• З отриманих даних вона повертає highschoolDBaddress.
2.
У наступному .then() [function (studentregistrationNumberANDhighschoolDBaddress)], використовується ця адреса для виклику та повернення `searchhighschoolcoursePromise()`.
-
Наступне .then(function (classList)) отримує
classInfo
для класів 3-го року. -
Нарешті, .then(function (classInfo)) витягує ім'я вчителя для вказаного класу і виводить його.
Promises обробляють асинхронні завдання у ланцюговому порядку, використовуючи функцію .then, де один .then() слідує за іншим, по черзі з'єднуючи завдання.
Однак, Promises не працюють в Internet Explorer без поліфілів або додаткових бібліотек, тому будьте обережні при використанні.
Async/Await для більш лаконічного і інтуїтивного виконання коду
function search_studentInfo_Promise (studentID) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "student-info/" + studentID,
function (response) {
resolve(response);
});
}
function search_highschool_DB_address_Promise (student_registrationNumber, highschool_name) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "highschool-db/" + highschool_name,
function (response) {
resolve ([student_registrationNumber, response]);
});
})
}
function search_highschool_course_Promise (student_registrationNumber,highschool_DB_address) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "classes/" + highschool_DB_address +"/"+ student_registrationNumber,
function (response) {
resolve(response);
});
});
}
function search_courseInfo_Promise (studentYear3_math_classCode) {
return new Promise(function (resolve, reject) {
ajax(baseUrl + "class-info/" + studentYear3_math_classCode,
function (response) {
resolve(response);
});
});
}
async function search_studentYear3_mathTeacher(studentID) {
let studentInfo = await search_studentInfo_Promise (studentID);
let highschool_DB_address
= await search_highschool_DB_address_Promise(studentInfo["highschool_name"]);
let classList
= await search_highschool_course_Promise(studentInfo["student_registrationNumber"], highschool_DB_address);
let courseInfo
= await search_courseInfo_Promise(courseList["Year3_math"]);
console.log(`Teacher: ${ courseInfo["teacherName"]}`);
}
search_studentInfo_Promise ("12345");
-
Додайте ключове слово
async
перед визначенням функції [function searchstudentYear3mathTeacher(studentID)]. Це дозволяє функції обробляти асинхронні завдання, як у синхронному коді. Всередині цієї функції ви можете писати асинхронні функції, як якщо б вони були синхронними. -
Використовуйте
const studentInfo
, щоб зберігати результат асинхронного виклику в функціїsearch_studentYear3_mathTeacher(studentID)
. Без ключового словаawait
змінна зберігатиме об'єкт Promise замість розв'язаногоstudentInfo
. Ключове слово await призупиняє виконання коду, поки асинхронне завдання не завершиться, дозволяючи отриматиstudentInfo
і зберегти його в змінній перед переходом до наступної операції. -
Аналогічно, використовуйте
await
, щоб отримати highschoolDBaddress, а потім отримати список курсів школи. -
Нарешті, витягніть courseInfo з списку курсів школи.
Коротше кажучи, await/async дозволяє писати асинхронний код в простий і інтуїтивно зрозумілий спосіб, що робить його схожим на синхронний код для кращої читабельності та підтримуваності.
Перекладено з: Function in Function, Callback function and Asynchronous Programming in JS