В JavaScript нам часто доводиться мати справу з асинхронною поведінкою роботи коду, що може призвести до плутанини для програмістів, які мають досвід роботи тільки з синхронним програмуванням. Ця стаття пояснить, що таке асинхронний код, розкаже про деякі труднощі використання асинхронного коду та способи вирішення цих проблем.
У чому різниця між синхронним і асинхронним кодом?
Синхронний код
У синхронних програмах, якщо у вас є два рядки коду (Рядок2 після Рядка1), то Рядок2 не може працювати до тих пір, поки Рядок1 не завершить своє виконання.
Наприклад, ви знаходитесь в черзі людей, які чекають, щоб купити квитки на поїзд. Ви не можете купити квиток на поїзд, коли всі люди перед вами ще не купили їх. Так само, як люди за вами не можуть купити квитки, поки ви ще не придбали ваш.
Асинхронний код
В асинхронних програмах, ви можете мати два рядки коду (Рядок2 після Рядка1), де Рядок1 містить команди, які будуть виконуватися в майбутньому, а Рядок2 працює до завершення команди в Рядок1.
Гарним прикладом є ситуація, коли ви їсте у ресторані. Інші люди можуть замовити їжу, але ви також можете її замовити. Вам не потрібно чекати, поки люди до вас завершать своє замовлення, щоб зробити те, щоб вам треба, і навпаки. Всі отримають свої замовлення, як тільки їжа буде приготована.
Послідовність, в якій люди отримують їжу часто корелюють з послідовністю, в якій вони замовили їжу, але ці послідовності не завжди повинні бути ідентичні. Наприклад, якщо ви замовляєте стейк, після чого я замовив склянку води, то, ймовірно, я отримаю моє замовлення першим, тому що, як правило, принести стакан води не займає так багато часу, як приготувати стейк.
Зверніть увагу, що асинхронний - це не те ж саме, що й одночасний або багатопотоковий. JavaScript може мати асинхронний код, але він, як правило, однопотоковий. Це як в ресторані з одним працівником, який робить все: подача і приготування їжі. Але якщо цей працівник працює досить швидко і може перемикатися між завданнями досить ефективно, то буде здаватися, що у ресторані є кілька робітників.
Приклади
Функція setTimeout
- найпростіший приклад асинхронного планування коду для запуску в майбутньому:
// Сказати "Hello."
console.log("Hello.");
// Сказати "Goodbye" через дві секунди.
setTimeout(function() {
console.log("Goodbye!");
}, 2000);
// Сказати "Hello again!"
console.log("Hello again!");
Якщо ви знайомі тільки з синхронним кодом, то ви будете очікувати, що наведений вище код запрацює таким чином:
- Сказати "Hello".
- Нічого не робити нічого дві секунди.
- Сказати "Goodbye!"
- Сказати "Hello again!"
Але setTimeout
не зупиняє виконання коду. Воно тільки "планує" те, що станеться у майбутньому, а потім зразу переходить до наступного рядка коду.
- Сказати "Hello."
- Сказати "Hello again!"
- Нічого не робити дві секунди.
- Сказати "Goodbye!"
Отримання даних за допомогою AJAX-запитів
Плутанина між роботою синхронного коду та асинхронного коду є типовою проблемою для початківців, які мають справу з AJAX-запитами в JavaScript. Вони часто пишуть JQuery код, який буде виглядати приблизно так:
function getData() {
var data;
$.get("example.php", function(response) {
data = response;
});
return data;
}
var data = getData();
console.log("The data is: " + data);
Це не буде працювати так, як очікується з точки зору синхронного програмування. Подібно setTimeout
в наведеному вище прикладі, функція $ .get
не зупиняє виконання коду, вона просто запускає код, коли сервер відповість на запит. Це означає рядок return data;
працюватиме перед data = response
, тому наведений вище код завжди буде виводити "The data is: undefined".
Асинхронний код потрібно структуровати по-іншому, і найпростіший спосіб це зробити - використати callback функції.
Callback функції
Припустимо, ви зателефонували своєму другові і попросили в нього деяку інформацію, наприклад, поштову адресу вашого спільного друга, яку ви загубили. Ваш друг її не пам'ятає, тому він повинен знайти свою адресну книгу і пошукати там. Це може зайняти кілька хвилин. Існують різні стратегії, як ви можете використати:
- (Синхронна) Ви залишаєтесь на зв'язку з ним і чекаєте, поки він шукає адресу.
- (Асинхронна) Ви кажете вашому другу, щоб зателефонував вам, як тільки він знайде адресу. Тим часом, ви можете зосередитися на інших завданнях, які необхідно зробити.
В JavaScript, ми можемо створити callback функцію, в яку ми передаємо асинхронній функції, які спрацюють, як тільки дана команда буде виконана.
Це означає, що замість:
var data = getData();
console.log("The data is: " + data);
буде:
getData(function (data) {
console.log("The data is: " + data);
});
Звичайно, як же getData
дізнається, що ми передаємо функцію? Як вона викликається, і як заповнюється параметр даних? На даний момент, нічого ще не відбулося; нам також потрібно змінити функцію getData
, щоб вона знала, що callback функція є її параметром.
function getData(callback) {
$.get("example.php", function(response) {
callback(response);
});
}
Ви можете помітити, що ми вже передавали callback функцію в $ .get
до цього. Ми також передали callback у функцію setTimeout(callback, delay)
у першому прикладі.
Оскільки у $.get
вже є callback, нам більше не потрібно вручну створювати ще одну в getData
, ми можемо просто безпосередньо передати callback функцію, яка була нам дана:
function getData(callback) {
$.get("example.php", callback);
}
Callback функції використовуються дуже часто в JavaScript, і якщо ви вже писали код цією мовою програмування, дуже ймовірно, що ви використовували їх (можливо, не знаючи про це). Майже всі веб-додатки використовують callback у event функціях (наприклад, window.onclick
), setTimeout
і setInterval
, або в AJAX-запитах.
Загальні проблеми з асинхронним кодом
Уникнення асинхронного коду
Деякі люди вважають, що мати справу з асинхронним кодом занадто складно, тому вони намагаються писати тільки синхронний код. Наприклад, замість того, щоб використовувати setTimeout
, ви можете створити функцію з синхронним кодом, яка буде нічого не робити протягом певного проміжку часу:
function pause(duration) {
var start = new Date().getTime();
while(new Date().getTime() - start < duration);
}
Крім того, при виконанні AJAX-запиту, легше зробити виклик синхронним кодом, ніж асинхронним (хоча браузери вже перестають підтримувати цю можливість). Є також синхронні альтернативи багатьох асинхронних функцій в Node.js.
Спроба уникнути асинхронного коду і замінити його синхронним майже завжди є поганою ідеєю в JavaScript, тому що ця мова програмування має тільки один потік (за винятком використання Web Workers). Це означає, що веб-сторінка не буде відповідати на запити під час роботи скрипту. Якщо ви використовуєте синхронну pause функцію наведену вище, або синхронний AJAX-запит, то користувач не зможе нічого робити під час виконання цих команд.
Ще гірше - коли використовується серверний JavaScript: сервер не може відповідати на будь-які запити під час очікування завершення синхронних функцій, а це означає, що кожному користувачеві, який відправляє запит на сервер, доведеться чекати, щоб отримати відповідь.
Проблеми з callback у циклах
При створенні callback всередині циклу, ви можете зіткнутися з несподіваною поведінкою роботи програми. Подумайте, що можна очікувати від коду нижче, а потім спробуйте запустити його в консолі браузера.
for(var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
По ідеї, програма повинна вивести наступні повідомлення з секундною затримкою:
1 second(s) elapsed.
2 second(s) elapsed.
3 second(s) elapsed.
Але результат буде наступним:
4 second(s) elapsed.
4 second(s) elapsed.
4 second(s) elapsed.
Проблема полягає в тому, що console.log(i + " second(s) elapsed");
знаходиться в callback асинхронної функції. Коли вона працює, for-цикл вже завершується, а змінна i
дорівнюватиме 4
.
Існують різні методи уникнення цієї проблеми, але найбільш поширеним є обернути setTimeout
у "перегородку", яка створюватиме нову область з різними i
в кожній ітерації:
for(var i = 1; i <= 3; i++) {
(function (i) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
})(i);
}
Якщо ви використовуєте ECMAScript 6 або пізніші версії, то більш елегантне рішення полягає у використанні let
замість var
, так як let
створює нову область для i
в кожній ітерації:
for(let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
Callback-пекло
Іноді у вас є ряд завдань, де кожен крок залежить від результатів попереднього кроку. Тому не важко отримати таке у синхронному коді:
var text = readFile(fileName),
tokens = tokenize(text),
parseTree = parse(tokens),
optimizedTree = optimize(parseTree),
output = evaluate(optimizedTree);
console.log(output);
При спробі зробити це в асинхронному коді, дуже легко отримати т.з. callback-пекло - розповсюджену проблему, коли callback функції "застрягають" глибоко всередині один одної. Node.js або front-end код з великою кількістю AJAX-запитів дуже часто під ризиком виглядати приблизно так:
readFile(fileName, function(text) {
tokenize(text, function(tokens) {
parse(tokens, function(parseTree) {
optimize(parseTree, function(optimizedTree) {
evaluate(optimizedTree, function(output) {
console.log(output);
});
});
});
});
});
Такий код важко читати, а спроба реорганізувати його, коли вам потрібно внести зміни, перетворюється на нестерпний головну біль. Якщо у вас є глибоко "заплутані" callback функції, як ті, що наведені вище, то буде, як правило, гарною ідеєю, організувати ваш код по-іншому. Існує кілька різних стратегій для цього.
Поділ коду на різні функції з відповідними іменами
Ви можете давати імена callback функціям, так щоб мати змогу посилатися на них. Це допомагає зробити код більш компактим і природно ділить код на невеликі логічні сектори.
function readFinish(text) {
tokenize(text, tokenizeFinish);
}
function tokenizeFinish(tokens) {
parse(tokens, parseFinish);
}
function parseFinish(parseTree) {
optimize(parseTree, optimizeFinish);
}
function optimizeFinish(optimizedTree) {
evalutate(optimizedTree, evaluateFinish);
}
function evaluateFinish(output) {
console.log(output);
}
readFile(fileName, readFinish);
Створення функції для запуску пайплайн завдань
Це рішення не таке гнучке, як попереднє, але якщо у вас є простий пайплайн асинхронних функцій, ви можете створити функцію, яка приймає масив завдань і виконує їх послідовно.
function performTasks(input, tasks) {
if(tasks.length === 1) return tasks[0](input);
tasks[0](input, function(output) {
performTasks(output, tasks.slice(1)); //Виконує команди в масиві 'tasks[]'
});
}
performTasks(fileName,
[readFile, token, parse, optimize, evaluate, function(output) {
console.log(output);
}]);
Інструменти розробки для роботи з асинхронним кодом
Async Бібліотеки
Якщо ви використовуєте багато асинхронних функцій, буде доцільно використовувати бібліотеку асинхронних функцій замість того, щоб створювати свої власні функції. Async.js - це популярна бібліотека, яка має багато корисних інструментів для роботи з асинхронним кодом.
Promises Promises є популярним способом позбавлення від callback-пекла. Спочатку це був тип конструкції, представлений такими бібліотеками JavaScript, як Q і when.js, але ці типи бібліотек стали достатньо популярними, що Promises тепер надаються і в ECMAScript 6.
Ідея полягає в тому, що замість того, щоб використовувати функції, які приймають вхідні дані і callback, ми створюємо функцію, яка повертає об'єкт promise, тобто, об'єкт, що представляє значення, яке буде існувати в майбутньому.
Наприклад, припустимо, що ми запускаємо функцію getData
, яка робить AJAX-запит і використовує callback звичайним способом:
function getData(options, callback) {
$.get("example.php", options, function(response) {
callback(null, JSON.parse(response));
}, function() {
callback(new Error("AJAX request failed!"));
});
}
// використання
getData({name: "John"}, function(err, data) {
if(err) {
console.log("Error! " + err.toString())
} else {
console.log(data);
}
});
Можна змінити функцію getData
так, щоб вона повертала promise. Для цього ми можемо створити promise з new Promise(callback)
, де callback - це функція з двома аргументами: resolve
і reject
. Ми будемо викликати resolve
, коли ми успішно отримаємо дані. Якщо щось піде не так, буде викликано reject
.
Після того, як ми отримаємо функцію, яка повертає promise, ми можемо використати метод .then
, щоб вказати, що має статися після виклику resolve
або reject
.
function getData(options) {
return new Promise(function(resolve, reject) { //створити новий promise
$.get("example.php", options, function(response) {
resolve(JSON.parse(response)); //якщо усе пішло, як заплановано
}, function() {
reject(new Error("AJAX request failed!")); //якщо щось пішло не так
});
});
}
// використання
getData({name: "John"}).then(function(data) {
console.log(data)
}, function(err) {
console.log("Error! " + err);
});
Обробка помилок тепер виглядає трохи приємніше, але важко зрозуміти, як ми робимо усе краще, враховуючи розмір функції. Усе стане ясніше, якщо ми перепишемо наш приклад callback-пекла за допомогою promise:
readFile("fileName")
.then(function(text) {
return tokenize(text);
}).then(function(tokens) {
return parse(tokens);
}).then(function(parseTree) {
return optimize(parseTree);
}).then(function(optimizedTree) {
return evaluate(optimizedTree);
}).then(function(output) {
console.log(output);
});
Ще немає коментарів