Калі вы толькі пачынаеце праграмаваць, хутчэй за ўсё, вы думаеце аб праграмах як аб наборы паслядоўных лагічных блокаў, дзе кожны блок робіць пэўную рэч і перадае свой вынік, каб наступны блок мог працаваць і гэтак далей, і для большай часткай вы маеце рацыю, большасць праграм выконваюцца паслядоўна, гэтая мадэль дазваляе нам ствараць праграмы, якія простыя ў напісанні і абслугоўванні. Аднак ёсць канкрэтныя выпадкі выкарыстання, калі гэтая паслядоўная мадэль не будзе працаваць або не будзе аптымальнай. У якасці прыкладу разгледзім прыкладанне для чытання кніг. Гэта дадатак мае некалькі дадатковых функцый, такіх як пошук усіх уваходжанняў слова, пераход паміж закладкамі і г.д. А цяпер уявіце, што карыстальнік зараз чытае доўгую кнігу і вырашае пашукаць, дзе сустракаецца агульнае слова, напрыклад «The». Прыкладанню звычайна спатрэбіцца некалькі секунд, каб знайсці і праіндэксаваць усе ўваходжанні гэтага слова. У паслядоўнай праграме карыстальнік не можа ўзаемадзейнічаць з дадаткам (змяняць старонку або вылучаць тэкст), пакуль не будзе выканана аперацыя пошуку. Спадзяюся, вы бачыце, што гэта не аптымальны карыстацкі досвед!
Дыяграма ілюструе тыповы працэс выканання прыкладання для чытання кніг. Калі карыстальнік ініцыюе працяглую аперацыю (у дадзеным выпадку пошук усіх уваходжанняў «the» у вялікай кнізе), праграма «завісае» на ўвесь час гэтай аперацыі. У гэтым выпадку карыстальнік будзе працягваць націскаць кнопку наступнай закладкі без выніку, пакуль аперацыя пошуку не будзе завершана, і ўсе аперацыі не ўступяць у сілу адначасова, ствараючы ў канчатковага карыстальніка адчуванне адстаючага прыкладання.
Вы маглі заўважыць, што гэты прыклад насамрэч не адпавядае паслядоўнай мадэлі, якую мы прадставілі раней. Гэта таму, што аперацыі тут не залежаць адна ад адной. Карыстальніку не трэба ведаць пра колькасць уваходжанняў «the», каб перайсці да наступнай закладкі, таму парадак выканання аперацый не вельмі важны. Нам не трэба чакаць заканчэння аперацыі пошуку, перш чым мы зможам перайсці да наступнай закладкі. Магчымае паляпшэнне папярэдняга патоку выканання заснавана на гэтай логіцы: мы можам запускаць аперацыю доўгага пошуку ў фонавым рэжыме, працягваць любыя ўваходныя аперацыі, і як толькі доўгая аперацыя будзе выканана, мы можам проста паведаміць карыстальніку. Паток выканання выглядае наступным чынам:
З такім патокам выканання карыстацкі досвед значна паляпшаецца. Цяпер карыстальнік можа ініцыяваць працяглую аперацыю, працягваць выкарыстоўваць прыкладанне ў звычайным рэжыме і атрымліваць апавяшчэнне пасля завяршэння аперацыі. Гэта аснова асінхроннага праграмавання.
Javascript, сярод іншых моў, падтрымлівае гэты стыль асінхроннага праграмавання, забяспечваючы шырокія API для дасягнення практычна любых асінхронных паводзін, якія вы можаце прыдумаць. У рэшце рэшт, Javascript па сваёй сутнасці павінен быць асінхроннай мовай. Калі мы звернемся да папярэдняга прыкладу, то асінхронная логіка ляжыць у аснове ўсіх прыкладанняў для ўзаемадзеяння з карыстальнікам, а Javascript у асноўным створаны для выкарыстання ў браўзеры, дзе большасць праграм рэагуюць на дзеянні карыстальніка.
Далей вы знойдзеце кароткае кіраўніцтва па асінхронным Javascript:
Зваротныя выклікі
У звычайнай праграме вы звычайна знойдзеце шэраг функцый. Каб выкарыстоўваць функцыю, мы выклікаем яе з наборам параметраў. Код функцыі будзе выкананы і верне вынік, нічога незвычайнага. Асінхроннае праграмаванне крыху зрушвае гэтую логіку. Вяртаючыся да прыкладу праграмы для чытання кніг, мы не можам выкарыстоўваць звычайную функцыю для рэалізацыі логікі аперацыі пошуку, паколькі аперацыя займае невядомую колькасць часу. Звычайная функцыя вернецца да таго, як аперацыя будзе зроблена, і гэта не тое паводзіны, якое мы чакаем. Рашэнне складаецца ў тым, каб паказаць іншую функцыю, якая будзе выканана пасля завяршэння аперацыі пошуку. Гэта мадэлюе наш варыянт выкарыстання, паколькі наша праграма можа працягваць свой паток у звычайным рэжыме, і пасля завяршэння аперацыі пошуку будзе выканана ўказаная функцыя, каб паведаміць карыстальніку аб выніках пошуку. Гэтую функцыю мы называем функцыяй зваротнага выкліку:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Спачатку мы вызначаем функцыю аперацыі пошуку, searchOccurrences. Патрабуецца слова для пошуку і другі параметр «зваротны выклік», які будзе функцыяй для выканання пасля завяршэння аперацыі пошуку. Функцыя пошукавай аперацыі наўмысна была абстрактнай, нам трэба засяродзіцца толькі на двух яе магчымых выніках: у першым выпадку ўсё прайшло паспяхова і ў нас ёсць вынік пошуку ў зменнай выніку. У гэтым выпадку мы проста павінны выклікаць функцыю зваротнага выкліку з наступнымі параметрамі: першы параметр мае нулявы характар, што азначае, што ніякай памылкі не адбылося, другі параметр - гэта слова, якое шукалі, а трэці і, магчыма, самы важны параметр з трох, - вынік пошукавай аперацыі.
У другім выпадку ўзнікае памылка, гэта таксама выпадак, калі выконваецца аперацыя пошуку і мы павінны выклікаць функцыю зваротнага выкліку. Мы выкарыстоўваем блок try and catch для перахопу любой памылкі і проста выклікаем функцыю зваротнага выкліку з аб'ектам памылкі з блока catch.
Затым мы вызначылі функцыю зваротнага выкліку, handleSearchOccurrences, захавалі яе логіку даволі простай. Гэта проста пытанне друку паведамлення на кансолі. Спачатку мы правяраем параметр «памылка», каб убачыць, ці не адбылася памылка ў галоўнай функцыі. У такім выпадку мы проста паведамляем карыстальніку, што аперацыя пошуку скончылася з памылкай. Калі памылак не было, друкуем паведамленне з вынікам пошукавай аперацыі.
Нарэшце, мы называем функцыю searchOccurrences словам "the". Функцыя цяпер будзе працаваць у звычайным рэжыме без блакіроўкі асноўнай праграмы, і пасля завяршэння пошуку будзе выкананы зваротны выклік, і мы атрымаем паведамленне з вынікамі пошуку або з паведамленнем пра памылку.
Тут важна згадаць, што мы маем доступ толькі да зменнай выніку ўнутры асноўных функцый і функцый зваротнага выкліку. Калі мы паспрабуем нешта накшталт гэтага:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
вынік друку будзе нявызначаным, таму што праграмы не чакаюць выканання функцыі searchOccurrences. Ён пераходзіць да наступнай інструкцыі, якая з'яўляецца аператарам друку, перш чым зменная выніку будзе прысвоена ўнутры асноўнай функцыі. У выніку мы надрукуем непрызначаную выніковую зменную.
Такім чынам, зыходзячы з гэтай логікі, мы павінны захаваць увесь код, які выкарыстоўвае зменную выніку, у функцыі зваротнага выкліку. Зараз гэта не можа здацца праблемай, але хутка можа перарасці ў рэальную праблему. Уявіце сабе выпадак, калі ў нас ёсць ланцужок асінхронных функцый, якія павінны працаваць паслядоўна. У тыповай логіцы зваротнага выкліку вы рэалізавалі б нешта накшталт гэтага:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Майце на ўвазе, што кожны зваротны выклік мае параметр памылкі, і кожную памылку трэба апрацоўваць асобна. Гэта робіць і без таго складаны код вышэй яшчэ больш складаным і складаным у абслугоўванні. Будзем спадзявацца, што Promises вырашае праблему пекла зваротнага выкліку, мы разгледзім гэта далей.
Абяцанні
Абяцанні будуюцца на аснове зваротных выклікаў і працуюць падобным чынам. Яны былі ўведзены як частка функцый ES6, каб вырашыць некаторыя абуральныя праблемы з зваротнымі выклікамі, напрыклад, пекла зваротнага выкліку. Абяцанні забяспечваюць уласныя функцыі, якія запускаюцца пры паспяховым завяршэнні (вырашэнні) і пры ўзнікненні памылак (адхіленні). Далей дэманструецца прыклад searchOccurrences, рэалізаваны з абяцаннямі:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Давайце разгледзім змены, якія мы ўжылі:
Функцыя searchOccurrences вяртае абяцанне. Унутры абяцання мы захоўваем тую ж логіку: у нас ёсць дзве функцыі resolve і reject, якія прадстаўляюць нашы зваротныя выклікі, а не адна функцыя зваротнага выкліку, якая апрацоўвае як паспяховае выкананне, так і выкананне з памылкамі. Абяцанні падзяляюць два вынікі і забяспечваюць чысты сінтаксіс пры выкліку функцыі main. Функцыя resolve «падключаецца» да асноўнай функцыі з дапамогай ключавога слова «then». Тут мы проста паказваем два параметры функцыі дазволу і друкуем вынік пошуку. Падобнае адносіцца да функцыі адхілення, яе можна падключыць з дапамогай ключавога слова «catch». Будзем спадзявацца, што вы зможаце ацаніць перавагі абяцанняў у плане чытальнасці і чысціні кода. Калі вы ўсё яшчэ абмяркоўваеце гэта, праверце, як мы можам вырашыць праблему зваротнага выкліку, аб'яднаўшы асінхронныя функцыі для выканання адна за адной:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Async/Await - гэта апошняе дадатак да нашай асінхроннай панэлі інструментаў у Javascript. Уведзеныя ў ES8, яны забяспечваюць новы ўзровень абстракцыі паверх асінхронных функцый, проста «чакаючы» выканання асінхроннай аперацыі. Паток праграмы блакуецца на гэтай інструкцыі, пакуль не будзе вернуты вынік асінхроннай аперацыі, пасля чаго праграма пяройдзе да выканання наступнай інструкцыі. Калі вы думаеце пра сінхронны паток выканання, вы маеце рацыю. Мы прайшлі поўны круг! Async/await спрабуе перанесці прастату сінхроннага праграмавання ў асінхронны свет. Калі ласка, майце на ўвазе, што гэта ўспрымаецца толькі ў выкананні і кодзе праграмы. Пад капотам усё застаецца па-ранейшаму, Async/await па-ранейшаму выкарыстоўвае абяцанні, а зваротныя выклікі з'яўляюцца іх будаўнічымі блокамі.
Давайце разгледзім наш прыклад і рэалізаваем яго з дапамогай Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Наш код асабліва не змяніўся, тут важна заўважыць ключавое слова «async» перад дэкларацыяй функцыі searchOccurrences. Гэта азначае, што функцыя асінхронная. Таксама звярніце ўвагу на ключавое слова «чакаць» пры выкліку функцыі searchOccurrences. Гэта загадае праграме чакаць выканання функцыі, пакуль не будзе вернуты вынік, перш чым праграма зможа перайсці да наступнай інструкцыі, іншымі словамі, выніковая зменная заўсёды будзе ўтрымліваць вяртанае значэнне функцыі searchOccurrences, а не абяцанне функцыя, у гэтым сэнсе, Async/Await не мае стану чакання, як Promises. Пасля завяршэння выканання мы пераходзім да аператара друку, і на гэты раз вынік фактычна змяшчае вынік аперацыі пошуку. Як і чакалася, новы код мае такія ж паводзіны, як калі б ён быў сінхронным.
Яшчэ адна нязначная рэч, пра якую трэба памятаць, заключаецца ў тым, што, паколькі ў нас больш няма функцый зваротнага выкліку, нам трэба апрацоўваць памылку searchOccurrences у той жа функцыі, паколькі мы не можам проста перадаць памылку функцыі зваротнага выкліку і апрацаваць яе там. Тут мы проста друкуем паведамленне пра памылку ў выпадку памылкі дзеля прыкладу.
Падвядзенне вынікаў
У гэтым артыкуле мы разгледзелі розныя падыходы, якія выкарыстоўваюцца для рэалізацыі асінхроннай логікі ў Javascript. Мы пачалі з вывучэння канкрэтнага прыкладу таго, чаму нам спатрэбіцца перайсці ад звычайнага сінхроннага стылю праграмавання да асінхроннай мадэлі. Затым мы перайшлі да зваротных выклікаў, якія з'яўляюцца асноўнымі будаўнічымі блокамі асінхроннага Javascript. Абмежаванні зваротных выклікаў прывялі нас да розных альтэрнатыў, якія былі дададзены на працягу многіх гадоў, каб пераадолець гэтыя абмежаванні, галоўным чынам абяцанняў і Async/await. Асінхронную логіку можна знайсці дзе заўгодна ў сетцы, незалежна ад таго, выклікаеце вы знешні API, ініцыюеце запыт да базы дадзеных, запісваеце ў лакальную файлавую сістэму ці нават чакаеце ўводу карыстальніка ў форме ўваходу. Будзем спадзявацца, што цяпер вы адчуваеце сябе больш упэўнена ў вырашэнні гэтых праблем, напісаўшы чысты і прыдатны для абслугоўвання асінхронны Javascript!
Калі вам спадабаўся гэты артыкул, зазірніце ў блог CLA, дзе мы абмяркоўваем розныя тэмы пра тое, як патрапіць у тэхналогію. Таксама праверце наш youtube-канал для нашых папярэдніх бясплатных семінараў і сачыце за намі ў сацыяльных сетках, каб вы не прапусцілі наступныя!
Распрацуйце сваю кар'еру на будучыню, палепшыўшы кваліфікацыю ў HTML, CSS і JavaScript з дапамогай Code Labs Academy Навучальнага кэмпа вэб-распрацоўкі.