Тази статия е моят опит да обясня някои основни аспекти на асинхронния код и как JavaScript работи зад кулисите. Така че нека започнем с удобна визуализация и известна терминология, преди да навлезем в дълбочина.

Голямата картина

Има много начини да визуализирате какво се случва тук, но аз особено харесвам следното изображение, защото дава ясна индикация в какъв ред се случват нещата и предоставя някои конкретни примери. Добра идея е да се върнете към това изображение, докато четете.

Първоначална терминология

Стек (наричан още повикващ стек): Структура от данни, която пази запис за къде се намирамев програмата. Тя избутва (т.е. добавя) неща върху горната част на стека и изскача (т.е. отнема) неща от горната част на стека.

  • Структура на данните: Формат/структура за организиране, съхраняване и управление на данни (напр. масиви, файлове, записи, таблици, дървета и др.).
  • Група:Основно неструктуриран регион на паметта (не е важен за тази статия… Споменавам го само защото е на изображението по-горе).

Уеб API: JavaScript от страна на клиента (т.е. вътре в уеб браузъри), който предоставя много API, които не са естествени за езика на JavaScript. Например вашият браузър вероятно има API за геолокация; който използва някакъв сложен C++ код от по-ниско ниво, за да ви позволи да извлечете данни за местоположение. Други често срещани API на браузъра включват DOM (който представя уеб страниците като възли и обекти) и API, които получават данни от сървъра.

  • API (Интерфейс за програмиране на приложения): API улесняват живота на разработчиците, като абстрахират сложния код, подобно на това как включването на нещо в контакт е по-лесно, отколкото физическото свързване на устройство директно към захранване. В допълнение към неща като API за геолокация, някои устройства също имат API за уведомяване, API за вибрации и т.н. Има и API на трети страни, които включват API на Reddit — позволяващ ви достъп до публикации от Reddit.com — и API на Twilio, който ви позволява всъщност изпращате съобщения до нечий телефон.

Опашка за обратно извикване (известна още като опашка за задачи/събития): Структура от данни, която съхранява задачи, докато не бъде изпратена до стека (чрез цикъла на събитията), когато стекът е празен.

Асинхронни задачи:Задачи, които могат да се изпълняват във фонов режим, за разлика от тези, които трябва да изчакат своя ред, преди да могат да се изпълнят (тези, които трябва да изчакат, се наричат ​​синхроннизадачи ).

Историята на цикъла на събитията

Подреждане в стека на повикванията

Влизането във функция я избутва в стека, докато връщанетоот функция я изважда (т.е. отървава се от нея). Така че погледнете следния грубо (и грубо) прекалено опростен код и помислете какъв ред на всяка част (A, B и C) е избутан в стека:

// A
const add = (a, b) => a + b;
// B
const explain = (a, b) => {
  console.log("Your result is", add(a, b));
}
// C
explain(2, 3);

Въпреки че функцията add (A) се появява първа в кода, тя е само дефиниция и следователно не се изпълнява. Не се извиква до дефиницията на функцията explain (B). Обаче explain не се извиква, докато C. Следователно C първо се избутва в стека. Тогава програмата вижда, че вътре в B се извиква A. B не може да заключи, докато A не е готово, така че A следователно се избутва отгоре на стека, дори над B. Следователно редът, който те се поставят в стека е: C → B → ︎ A. Ако имаме тези функции в програма, наречена „main“, тогава ето моментна снимка на това как ще изглежда стекът след стартиране на main.

Тъй като всяка от по-ниските задачи не може да се изпълнява до приключването на по-високите, add (A) се разрешава първо (т.е. тя е „отгоре“ на стека, така че се изважда от стека първо). След като add бъде разрешен и изваден от стека, explain ще се изпълни след това, извеждайки „Вашият резултат е 5.“ След това mainще завърши изпълнението си (тъй като няма друг код в програмата) и след това стекът ще бъде празен. След като е празен, цикълът на събитията ще се активира.

Цикъл на събитията

Event Loop е механизъм, който първо изчаква стека да бъде празен. Когато е празен, Event Loop избутва първата задача от опашката за обратно извикване (ако има такава) в стека, така че да може да бъде изпълнена. В зависимост от това каква е тази задача, това може да постави началото на изцяло нов цикъл от събития. Например, ако сте вложили няколко setTimeouts един в друг (което може да означава, че се нуждаете от психиатрична оценка), стекът първо ще се изпразни и след това цикълът на събития ще грабне функцията за обратно извикване, която чака в опашката за обратно извикване, и ще повтори процеса, докато нищо не остане в опашката (и следователно в стека).

Опашка за обратно повикване

Асинхронните задачи (напр. setTimeout(), AJAX заявки) се изпращат първо към опашката за обратно извикване, където може да висят известно време. Това позволява на стека да продължи да изпълнява задачи (защото в противен случай ще блокира работата на програмата, докато не бъде завършена каквато и да е бавна задача). Когато стекът е празен, цикълът за събития, както е обяснено по-горе, изпраща първото нещо в опашката към стека.

Това е добър момент да поговорим за понятията FIFO (първи влязъл, първи излязъл)иLIFO (последен влязъл, първи излязъл). Както е описано по-горе, стекът се изпълнява последното (т.е. най-високото) нещо, което се появява първо в стека. Следователно стекът е „LIFO“.

Опашката за обратно извикване обаче е малко по-различна. Ако задачите се натрупват в опашката, коя задача отива в стека? (Забележка: скрито вече дадох отговора по-горе) За щастие нашата диаграма по-горе ни показва много ясно — това е първата задача (т.е. най-лявата задача), която цикълът на събитията изпраща към стека . Следователно опашката всъщност е „FIFO“.

Сега нека приложим всичко това на практика...

Поп тест (и обяснение) с setTimeout

Въпрос

Предварително определената функция setTimeout приема два аргумента. Вторият аргумент е число (милисекунди), а първият аргумент е функцията за обратно извикване, която ще бъде изпълнена след това определено число. След като обяснихме това, често срещан въпрос в техническите интервюта включва настройката setTimeout() на нула. Например: Какъв е резултатът от функцията по-долу?

const ohGodItsAnEventLoopQuestion = () => {
   console.log(1); 
   setTimeout(() => { console.log(2); }, 1000); 
   setTimeout(() => { console.log(3); }, 0); 
   console.log(4);
}

ohGodItsAnEventLoopQuestion(); 

Отделете 30 секунди, за да отговорите на това - вероятно няма да ви трябва повече. Или го знаете, или не. (Отговорът е даден в края - не превъртайте надолу, докато не го опитате сами!)

Допълнително обяснение и отговор

Вярно е, че JavaScript ни позволява да правим само едно нещо в даден момент (т.е. това е „еднонишков“ език, което е все едно да кажем, че има само един стек за извикване)… но вашият браузър включва множество невероятни уеб API, като например DOM, AJAX и setTimeout. (Забележка:Еквивалентът на Node на Web API са неговите C++ API, които могат да ви осигурят допълнителни страхотни функции)

Когато асинхронна функция се извика в стека, тя използва подходящия уеб API (т.е. setTimeout) и след това съответната задача (т.е. console.log вътре в setTimeout) отива в опашката за обратно извикване. След това Event Loop го премества в стека, където може да бъде изпълнен.

Поради тази причина, дори когато setTimeout е настроен на 0 (т.е. „изчакайте нула милисекунди“), задачата ще предприеме същото пътуване, споменато в предишния параграф (тъй като setTimeout е уеб API), като по този начин идва след всичко останало в стека се изпълнява първо. Защо? Тъй като, както споменах по-рано, цикълът на събития премества нещо от опашката за обратно извикване в стека само ако стекът е празен. Така че можете да мислите за цикъла на събитията като нещо, което премества задачите в опашката за обратно извикване в празен стек за повиквания.

С други думи, дори ако вашият втори аргумент в setTimeout е 0, това всъщност не означава „изчакайте нула секунди за изпълнение (т.е. изпълнете незабавно)“. Вместо това е по-точно да мислите за това като за „изчакайте минимум нула секунди, но първо изпълнете всичко, което е в стека“. Поради тази причина отговорът на въпроса от интервюто по-горе е: 1 4 3 2.

Последни мисли

Тази статия включваше много повторения, защото такава неинтуитивна концепция може да отнеме много време на начинаещите, за да я разберат напълно (оттук и фактът, че дори старши разработчици биват питани за тази тема). Но един ефективен начин да научите нещо е да видите множество обяснения на една и съща тема по различни начини. Поради тази причина ви насърчавам да потърсите другаде други обяснения или контекст, ако имате нужда от допълнителни обяснения.

Но със сигурност се надявам, че това обяснение ви дава добра представа за основите на това как работят цикълът на събитията и setTimeout... защото почти мога да гарантирам, че ще бъдете попитани за това по време на интервю в някакъв момент от вашата кариера в разработката.