Галоўная / Артыкулы

Розумэнне клаузураў у JavaScript і циклу падзей.

Клозуры ў JavaScript захоўваюць зменныя з зовнішньага прыметніка жывымі; цыкл змагання аранжуе стак абыявленняў, мікрозадачы і макрозадачы. Аба падыякства паяснююцца прыкладамі коду, які можна запрацаваць.

Выдавец: REACTAPP.TOP16 хв чытання3605 слоў

Крыніцы: JavaScript Closures Explained in 2 Minutes, The JavaScript Event Loop — Finally Explained With Zero Confusion

Ява-скрыпт часта практычна апісваяць як мову, наполненую страннасцямі, але два з яё найбольш неправильна разумеваных аспектаў — замыканні і цикл запуску падзеяў — на самай працо выходзяць з адной і той жа основнай ідеі: функцыі і запланаваная робота не зникаюць проста таму, што код, які іх абрамляе, працюе далей. Ёсць неабходнасць разумець, як функцыя можа „памятаць“ зменныя пасля таго, як яе бацька-функцыя завершылася, і як система запуску выбірае момент для адкрыцья коду, які не готавы негайнаа, для чаго трэба перестаць спрыятаць да Ява-скрыпту як да простага скрыпта, напісанага рэчынкамі, і пачаць спрыятаць да яе як да системы з правіламі памяці і таймінгу. У этым артыкуле рассматраюцца обая ідеі, пачынаючы з замыканняў.

Разуменне замыканняў

Што такое замыканне на самай працо

Закрыцея вырабатваецца тады, калі функцыя зберагае зменныя з тэрыторіі, дзе яна была створана, і можа даўжыну до яных нават пасля таго, как зованая яе функцыя вучыніла сваю роботу.

Прыемны спосаб разумець гэта – це функцыя, якая носіць з сабой «маленькі рюкзак» з зменнымі, куды бы ні пайшла.

function createCounter() {
  let count = 0;
return function () {
    count++;
    return count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Заўважыце, што createCounter() выканана і вяртаеся, але внутраняя функцыя, яку яна вяртае, все ўсё мае доступ да count. Гэты стойкі доступ, пасля таго як зованне звычайнае функцыі тэхнічна завершылася, і ёсць закрыцея.

Тыповыя ситуацыі, калі вырабатваецца закрыцея

Закрыцеі маюць значэнне тады, калі функцыя павінна зберагаць які-небудзь даныя пасля таго, как функцыя, якая яе створыла, вучыніла сваю роботу. Вы зустроцеце гэты патерн у некалькіх повсякдзенных сцэнарыях:

  • Прыватныя зменныя – спрятванне дадзэнняў, каб код звануты званне не мог лягча да яных дасягнуць.
  • Фабрыкі функцый – стварэнне індывідуальных функцый на адказ да параметраў.
  • Калебакі – зберагчыце значэнні, каб яны застаўаліся доступнымі калі калебак урэшце выканаецца.
  • Таймеры – доступ да зменных з усероўні setTimeout або setInterval.
  • Адпрацоўвальнікі змаганняў – запамятовванне дадзеных, звязаных з конкрэтным змаганнем.
  • React – розумеўце калебакоў, эфектаў і проблемы „застарэлага замкнення“.
  • Чаму яны важны

    Замкненні дазваляюць зберагчы стан без апыткі на глобальныя зменныя, што робіць ваш код чыстэйшым і безпечнейшым.

    function createAccount(initialBalance) {
      let balance = initialBalance;
      return {
        deposit(amount) {
          balance += amount;
        },
        getBalance() {
          return balance;
        }
      };
    }
    const account = createAccount(100);
    account.deposit(50);
    console.log(account.getBalance()); // 150
    console.log(account.balance);      // undefined
    

    У тамтам прыкладзе balance не можа быць доступным безпасова з усероўні функцыі. Толькі методы об’екта, якія вяртаюцца з createAccount, маюць да яго доступ.

    Такая структура дае вам калькольвыя рэальныя прынтэры:

    • Лепшая інкапсуляцыя
    • Праўdziва прыватнае становішча
    • Простейшы, лепш заўважальны код
    • Меньшая залежнасць ад глобальных зменных

    Класычны прыклад цікла

    Яскравы прыклад выкарыстоўвання клаузур — это разлік межа выкарыстоўванням var і let у цікле, який плануе калебэксы.

    Што выходзіць з var:

    for (var i = 1; i <= 3; i++) {
      setTimeout(() => {
        console.log(i);
      }, 1000);
    }
    // Output:
    // 4
    // 4
    // 4
    

    Рэзультат не ўсё тое, чаго большасць людзей спачатку чакае, і прычына ў тым, што var мае дыяпазон функцыі, а не блока. Усе тры калебэксы setTimeout у падсумку абыраюць тую ж самую зменную i. Калі хоць адны з іх нарэшце выканаецца, цікл уже завершыўся, а i вярнуўся да значэння 4.

    Тепер паўпоручыце з let:

    for (let i = 1; i <= 3; i++) {
      setTimeout(() => {
        console.log(i);
      }, 1000);
    }
    // Output:
    // 1
    // 2
    // 3
    

    Паколькі let мае блак-скоуп, кожны раз праходжэння праз цикл стварыя самастоятельную прыязню зменнай i. Гэта значыць, што кожны калебак закрываеся над разным значэнням, і цифры выдрукваюцца у тым порядку, які мы і так моглі спадзявацца.

    Тыповая розмова пад час адзінкавання

    Запытанне: Чаму ў JavaScript існуе паняванне?

    Паняванне — это функцыя, якая пам’ятае і можа ўсё ж такі доступваць да зменных з таго скоупу, у яком была задана, нават пасля таго, як зовнішняя функцыя вже завершила свою експансію.

    Даадзейнае запытанне: Дзе на практыцы вы вжываеце паняванні?

    Хораша адказка охопляе практычны выкарыстоўванне, а не толькі вялічыну:

    Закрыцці частае явішчаюцца ў функціях-зворотных вызовах, таймерах, обробніках змян і фабрыкі функцый. Яны таксама корыстны, калі якая-небудзь частка стану павінна застацца прыватной, а не знаходзіцца ў глобальнай сферы.

    Асновная ідея

    Сутнасць гэтай ідэі можна выразіць за дапамою адной рэвалюцыі: функцыя плюс зменныя, якія ў яе є, равнаюць закрыццю. Якщо абразат ранца застаецца ў вашай галаве — функцыя, якая носіць з сабой захопленыя зменныя куды б яе не вызвалі — то весь гэты концэпт становіцца значна менш загадковым.

    Розумеўце ціклу змян

    Закрыцці поясняюць, як функцыя може зберагаць данні праўдачы часу. Цикл змян поясняе, як JavaScript вырашае коли насправды запускаць код, який не готавы негаразду — і самэлькі тут выходзяць багатыя плутаныя выведы з консолі.

    Практычна кожны разработчык JavaScript раней чым пазней сталкаецца з такім фрагментам коду, чырвоны ў інтэрв’ю або проста випадкова:

    console.log("1");
    
    setTimeout(() => {
      console.log("2");
    }, 0);
    
    console.log("3");
    

    Якщо чытаць гэты код зверху вніз, можна спадзявацца, што консоль выведзе 1, потым 2, а пасля 3 — таймаут заданы на нуль мілісекунд, таму не павінна быць жадная затрымка.

    Але як толькі запустыць гэты код у консолі браузера або в Node, рэальны выхід будзе такім:

    1
    3
    2
    

    Тады чаму код, запланаваны да выконання "негайна", на самом дзеле выконваецца &em>пасля коду, напісанага пазней у файле?

    Ёжыць на гэты вопыт трэба разумець механізм, які працуе пад вашым кодам — цикл задач. У зменшэнні на формальную термінологію пра контексты выконання і алгоритмы планавання, кароткі та зрозумелы ментальны модель таго, што на самом дзеле відбываецца, є болей корыстным.

    Асновная супераспакоўка ў самай суті JavaScript

    Перш чым перейсці да дыяграм і адзначэнняў, ёсць адна практычная правда, якую трэба усвядоміць:

    JavaScript ў строгам сэнсе являе ся адной-нітковым працоўнікам.

    Адной-нітковы працоўнік значыць, што JavaScript работае з адной толькі стаканам вызваў. У яго є адна „галава“ для обробкі дадзеных, і ён можа выконваць толькі адну лінію коду ў кожны момент часу. Ён не можа запускать две функцыі паралельна, а таксама не можа самастоятельна раздзеліць вычысленні межаў калькуляторных ядраў.

    Гэтыя абмежэння паднімаюць яўны вопыт. Уявіце калекцыю коду, якой трэба чакаць тры цэлыя секунды, пакуль повольны API даа адказ — калі є толькі адна нітка і няма нічога іншага, што магло бы дапамогць, што ж тады зможа робіць рэшта сторанкі пад час гэтага чакання?

    У суцэльнае адной-нітковай средзе без жадных дапамог увесь прыглед заціснуеся. Вы не зможаце нічога клікнуць, прасунуць сторонку, запісаць тэкст у поле чыта працаваць анімация — усё застоіцца, пакуль нарэшце не прыйдзе адпаведны адказ з сеті.

    Але на самай працэ так не выглядае. Сучасныя веб-застосункі рэгулярна стрімуюць відэа, аплоўваюць запытамі да сеті у фоне, тыха апдэйтуўваюць статыстыку і прымоюць данні з клавіатуры адночасна, не зацісняючыся.

    Тады як мова з адной ніткай гэта робіць?

    Адказ: сам JavaScript ёсць адной-нітковы, але среда, якая яго отачвае — прыглед чы Node.js — ніякой адной-нітковай не ёсць.

    Кухня рэстарана як ментальны модель

    Найяснейшы спосаб зрозумець гэтую архітектуру — це уявіць сабе зайнятую кухню фаст-фуда:

    [ Customer Orders ] ──> [ The Solo Chef ] (Call Stack)
                                  │
                                  ▼ Hands off slow jobs
                            [ The Kitchen Staff ] (Web APIs)
                                  │
                                  ▼ Ready order slips
                            [ Order Counter ] (Queues)
                                  ▲
                                  │ Checks constantly
                            [ The Floor Manager ] (The Event Loop)
    

    Ось як распадаюцца ролі:

    1. Сам пахар (стак дзвянакоў). Шырокі і майстрацкі, але можа працаваць толькі над аднам замовленнем за раз — завершыўшы яго цэлкам, перш чым прыступіць да наступнага.
    2. Памочнікі на кухні (Web APIs). Калі замовленню трэба дагучыць дзяўна — таймер або медленная адпаведзь водзія-дазваžчыка — пахар не стоўці і не стагнае, ліпш я ўзяўшы замовленне перадае яго памочніку з інструкцыямі старацца за яго і дапавесті, калі ён будзе гатовы, а сам вяртаецца да наступнай задачы.
    3. Ліст замовленняў (чэрыны вызваў і мікразадач). Калі таймер завершыў сваю роботу, памочнік не перарывае пахара падчас выпання задачі, не прыкладаючы замовленне яму ў рукі. Гэта стварыла б хаос. У замяну гатовае замовленне кладзецца на ліст, каб чакала.
  • Менеджер падчысткі (цикл захода). Штырэнны менеджер пастой хадзіць туды-сюды межа стойкай і шэф-кухаром, дазвольваючы толькі аднам строгам правілу: ніколы не перакрываць шэф-кухара, калі ён працуе. Як толькі рукі шэф-кухара абсолютна вольныя, менеджер берае наступную квітанцыю, якая чакае на стойкай, і перадае ёю шэф-кухара.
  • Гэта ўсё система у мініятуре. Урахавляючы сцэнарый ресторана, прыйшла час заменіць гэтыя ролі на ўсёрасовыя назвы ў двайчыце-мовнам машынным кодзе.

    Машынны код называе гэтыя чатыры элементы розна, але кожны з іх безпосередньа адпавядае певной ролі ў ресторане.

    Нижэйшыя схемы паказваюць, як серавэрны аптэка (браузер чым Node.js) абгружае двайчыце-мовны машынны код і з’ѐднае гэтыя элементы адзін з другім.

    ┌───────────────────────────────────────────────────────────┐
    │                     BROWSER / RUNTIME                     │
    │                                                           │
    │   ┌─────────────────┐             ┌───────────────────┐   │
    │   │   CALL STACK    │             │     WEB APIs      │   │
    │   │ (JS Engine/V8)  │             │                   │   │
    │   │                 │ ──────────> │  setTimeout()     │   │
    │   │  fnA()          │             │  fetch()          │   │
    │   │  console.log()  │             │  DOM Events       │   │
    │   └────────┬────────┘             └─────────┬─────────┘   │
    │            ▲                                │             │
    │            │                                │ Finished    │
    │            │                                ▼             │
    │            │                      ┌───────────────────┐   │
    │            │                      │  MICROTASK QUEUE  │   │
    │            │                      │  (Promises, VIP)  │   │
    │            │                      └─────────┬─────────┘   │
    │            │                                │             │
    │      EVENT LOOP                             ▼             │
    │   "Is Stack empty?"               ┌───────────────────┐   │
    │            └───────────────────── │  MACROTASK QUEUE  │   │
    │                                   │ (Timers, Clicks)  │   │
    │                                   └───────────────────┘   │
    └───────────────────────────────────────────────────────────┘
    

    Стак вызоў — „всё, што зараз адбываецца“

    Стакан вызваў ёсць звычайная структура типу LIFO (Last In, First Out), якую сама энцымэт JavaScript кантролюе — гэты той самы механізм, які викорыстоўваецца ў V8 у Chrome і Node, або JavaScriptCore у Safari.

    Кожны раз, калі вы вызываеце функцыю, яна дадаецца на верхнюю частку гэтага стакана. Калі функцыя вяртаецца або завершае свою роботу, яна выкарыстоўваецца з верхней часткі.

    function stepThree() {
      console.log("Done!");
    }
    
    function stepTwo() {
      stepThree();
    }
    
    function stepOne() {
      stepTwo();
    }
    
    stepOne();
    

    Аналіз невялікага ланца вызваў паказвае механізм чыста:

    • stepOne дадаецца на стакан.
    • stepOne вызывае stepTwo, таму stepTwo таксама дадаецца на верхнюю частку.
    • stepTwo вызывае stepThree, таму stepThree дадаецца ўсё на верхнюю частку.
    • stepThree выкананае console.log, якае выводзіць "Done!" і пасля таго выкарыстоўваецца зі стакана.
  • stepThree завершаецца, таму ён выкарыстоўваецца.
  • stepTwo завершаецца, таму ён выкарыстоўваецца.
  • stepOne завершаецца, таму ён выкарыстоўваецца.
  • Стак вяртаецца да стану пустога.
  • Якщо якая-небудзь функцыя на гэтым стаке патрабуе, скажамо, дзесяць секунд аднойчынага інтэнсівнага вырахунку, весь час адработкі замарзае на гэтыя дзесяць секунд. Нічога іншага не можа работаць пакуль стак не будзе спражнены — гэта прымітны наследак таго, што «кухар» мае толькі два рукі.

    Web API-яў — дзе на самай працоўцы відбываецца „чаканне“

    Этага факту часта не спадзяюцца багато людзей на пачатку сваей кар’еры: setTimeout, fetch і абэрненты запуску падзеяў DOM не ёсць функцыямі самай мовы JavaScript.

    Їх няма нідзе ў спэцыфікацыі ECMAScript. Їх з’яўляеся та среда, якая хостуе ваш код — прыгледач, а ў разе Node.js — сумяшанне C++ біндынгаў і бібліятэкі libuv.

    Разглянем гэты вызыв:

    setTimeout(callback, 2000);
    

    Калі стак вызываў выканае тыя setTimeout, ён на самай працоўцы толькі паведамляе внутраній механізм таймера прыгледача пачаць звяртанне 2000 мілісекунд. Як толькі гэтае запаведзенне перадаецца, сам setTimeout адразу ж выкарыстоўваецца зі стака вызываў, і выкананне продзейнае да наступнага элемента вашага коду. Сама звяртання адбываецца паўнасцю за межамі адносны лямкі JavaScript, у средзе-хосты.

    Очакванне макратаскаў — звычная лінія

    Гэтае очакванне таксама называецца очакваннем калебэкаў або очакваннем задаць, залежна ад таго, каго вы спытваеце.

    Калі Web API завершыць свою фонавую задачу — таймер дасягне нуля, падае меры сетавага адпаведзя, корыстнік нажме на ўсё-што-небудзь — сяродовышча хоста пакладзе адпаведны калебэк у гэтыя чергу.

    Усе, што знаходзіцца ў гэтыяй чергу, паводзіцца прыязна: цэў стандартныя лінія FIFO (First In, First Out), дзе кожны элемент чакае своей парты, каб яго запрацавалі ў Call Stack.

    Тыповыя прыклады макрозадач включаюць:

    • Калебэкі, запланаваныя за дапамогою setTimeout() і setInterval()
    • Прыёмнікі запускаў DOM, такія як click, scroll або keydown
    • setImmediate(), які є спецыфічны для Node.js

    Чэрга мікрозадач — прыбліжаная дорага

    Паралельна той стандартной черге існуе меншая черга з набліжна вышэйшай прыоритэтам: чэрга мікрозадач.

    Усе, што размешчаецца тут, фактычна пераводзіцца на пачатак лініі. Калі стек вызываў становіцца порожнім, цыкл адбытка спачатку пераглядае калекцыю мікрзаданняў — прычаму нават не розглядаючы тое, што чакае ў калекцыі макрзаданняў.

    Элементы, якія потрапляюць у калекцыю мікрзаданняў, включаюць:

    • Обробнікі, прыўязаныя через .then(), .catch() або .finally() да Promise-аў
    • Код, які выконваецца пасля await унутры функціі async
    • Усе, што запланавана за дапамою queueMicrotask()
    • Функціі-вызывы, якія запускаюцца MutationObserver

    Правіла, якія керуюць цыклам

    Якшчо адмахнуцца ад метафор, цыкл адбытка на самай працоўцы ёсць проста бесканечны while-цикл, які выконваецца ўнутры сераўіснага сераўісу, і які павтарае пяць строгіх крокаў:

    1. Апавяць стакан вызоў. Продовжыце выконваць сінхронны код пакуль на стакане абсалютна нічога не застанется.
    2. Полныя апавяць календар мікрзадач. Колькі хоць адна мікрзадача чакае, яе трэба выконваць. Якщо ў результате её выканання з’яўляецца ўсё новая мікрзадача, яе таксама трэба выконваць. Цей цыкл не перайдзе да наступнага крока, пакуль у календары мікрзадач не застанется нічога.
    3. Адмаўляцца рэндарынгу, якщо вы ў браузеры. Энжын пераканальваецца, час абнаўліць экран — зазвычай 60 разоў за секунду, працэса па 16,6 мілісекунд — і як толькі так, ён перыяканальвае лейаут і абнаўляе змест.
    4. Выконваць роўна адну макрзадачу. Паглядзіце на календар макрзадач, выберыце тую задачу, якая чакае дзейшчы, падаць яе на стакан вызоў і дазволіць яй выконвацца да завершэння.
    5. Пачаць цыкл знову з першага крока.

    Тое, што спантывае людзей, — гэта асіметрыя: мікразадачы выкаранчаваюцца цэлком, кожная з іх, прычаму цикл не пераходзіць да наступнага крока — але макразадачы обрабоўваюцца строго по адной, пры чым пасля кожнай з іх выканана ўсеяна перапачатковая перагляд стаку і очэредзі мікразадач.

    Разбор вядомай задачы з адбору

    Калі механізмы вже з’ясаваны, цікава будзе рассмотрэць фрагмент коду, який спантывае вельмі большую колькасць разработчыкаў:

    console.log("Start");
    
    setTimeout(() => {
      console.log("Timeout Callback");
    }, 0);
    
    Promise.resolve().then(() => {
      console.log("Promise Callback");
    });
    
    console.log("End");
    

    Працаваючы лінія за лініяй з чатырма складовымі, можна зрозумець прычыну неспадзяванага рэзультата.

    Спачатку сінхронны прайс

    console.log("Start") дадаецца у стак, выведае "Start", а пасля — выкаранчаваецца зі стаку.

    Далей на стакан падае setTimeout(...). Ён наказвае слой Web API браузера запусціць таймер на 0 мілісекунд з заданым функцыяй-выклікам. Паколькі затрымка доравна нулю, той таймер практычна адразу завершаецца на фоне, таму браузер негайна падае () => console.log("Timeout Callback") у чергу Macrotask. Сам вызов setTimeout пасля чаго выймаецца зі стакана.

    Пасля чаго на стакан падае Promise.resolve().then(...). Праміс вирашваецца мгновэна, а його обрабоўчык .then() — () => console.log("Promise Callback") — падае безпосередна ў чергу Microtask. Штыр вызов таксама выймаецца зі стакана.

    На завершанне падае console.log("End"), які выведае "End", і пасля чаго выймаецца зі стакана.

    Калі стакан спрабудзецца

    У гэты момант у стаке вызваў не застаёцца нічога. Цыкл змаганняў працягвае роботу і пераглядае сваі два кялі: у кялі мікрозадач стоіць функцыя-выкалканне для Promise, а ў кялі макрозадач — функцыя-выкалканне для таймауту.

    Мікрозадачы выкананыя першыя

    Цыкл спачатку пераглядае кялі мікрозадач. У ёму знаходзится функцыя-выкалканне для Promise, яна дадаецца у стак вызваў, і выведаецца „Promise Callback“. Стак знову апошнюе, і тепер кялі мікрозадач таксама стае порожнім.

    Потым — адна макрозадача

    Калі мікрозадачы ўжо немае, цыкл нарэшце пераходзіць да кялі макрозадач. У ёму бераецца старэйшы элемент — функцыя-выкалканне для таймауту, яна дадаецца у стак вызваў, выведаецца „Timeout Callback“, а пасля — знімаецца зі стака.

    Якшто адразу сумаваць усё, то фінальная последовасць выведання будзе такой:

    Start
    End
    Promise Callback
    Timeout Callback
    

    Нават як таймаут быў заданы на 0 мілісекунд і запісаны ранейш за прагненне ў коде, калбэк прагнення вывелі першы. Це паўтарае правілу, што мікрозадачы завжды виконваюцца ранейш за макрозадачы, незалежна ад таго, насколькі маленькі затрымкі вы задаеце таймеру.

    Гэта прыводзіць да распашчанага нерозумеўнага ставлення да таго, што на самай працэ практыкуе значэнне затрымкі. Багато пачатківцяў вырашваюць, што setTimeout(fn, 1000) означае "выконайце гэтую функцыю роўна 1000 мілісекунд зараз". Аднак так не ёст. Насправды гэта гарантуе, што калбэк не будзе дадзен у чергу задач тымчасова, пакуль не прыйдзе хоць 1000 мілісекунд. Калі ён ужо опыніцца ў черзе, яму все рава трэба чакаць своей разы, пакуль стек вызываў не будзе порожнім і ўсе незавершаныя мікрозадачы не будуць виконаны.

    Якщо стакан вызваў у вас зайняты пры выкарыстоўванні складнага алгорытму сортавання чыя-небудзь зображэння праўе 5 секунд, тады функцыя setTimeout(fn, 100) застанецца у черзі без дзейнасці праўе цяперышнія 5 секунд, прычаму цикл запуску змаганняў нават не зможа яе пераглянуць. Корача кажучы, час затрымкі ўвогуле яўляе сабою мінімальны час чакання, а не гарантаваны час ўжывання.

    Такая практыка спалювання элементаў у черзі мае ўсё болючыя побачныя эфекты, якія варта розумець окрема. Паколькі цикл запуску змаганняў адмовляецца пераходзіць да наступных задач, пакуль черга мікразадач не стане абсалютна порожней, можа выйсці так, што ваша прыкладна програма заўсёды застанецца заблокаванай, хоця тэхнічна синхронны стакан вызваў не будзе заблокаваны. Такі спосаб абяцкання называецца голадам мікрозадач, іноды — голадам черги.

    Уявіце сабе такую функцыю:

    function infiniteMicrotasks() {
      Promise.resolve().then(() => {
        infiniteMicrotasks();
      });
    }
    
    infiniteMicrotasks();
    

    Разбір таго, што адзейнаецца: пачатковы вызов вяртаецца, і стак развальваецца. Пасля чаго цыкл адзейнаў выканае перагляд Календаря мікрозадач і знаходзіць яе елемент. Ён выканае гэты элемент, але яго негайны выконанне плануе ўтрохту іншую мікрозадачу. Цыкл адзейнаў знову пераглядае календар, бачыць, што ён яшчэ зайняты, і выканае наступную мікрозадачу — якая зноў плануе ўтрохту. Стак продовжае развальвацца пасля кожной мікрозадачы, але цыкл адзейнаў ніколі не выходзіць за межы гэтага крока перагляду. Ён ніколі не просуваецца дасканальна, каб браузер могаў перамалюваць інтэрфейс аб дасягнуць чаго-небудзь, што чакае ў Календары макрозадач.

    Практычны наследкі — сторанка выглядае абсалютна замарожанай. Клікі ігноруюцца, анімацыі перестаюць працаваць, і ў канечнай лініі браузер можа павесты ўпаведамленне пра скрыпт, які не рэагуе.

    Ёнколі, каб параксануць, як кожны тип задач плануецца і прыорітэзуецца працуючы програме, корыстна ўважна паглядзець на апісанне синхроннага коду, мікрозадач, макрозадач і процэса атрыбутавання ў рамках ціклу, як паказана на супаўязанай дыяграме.

    Розумэнне таго, як працуе цікл змян, — гэта не проста карысная інформація для спаканаванняў; гэта таксама безпасебна важліва для проектавання рэальных прыкладоў практычнае ведамасць, а з яе выклікаюцца тры практычныя прыемы.

    Паўначальнае — разбіць дорогі сінхронныя цыклы. Якщо вам трэба обрабатваць масів з, скажымо, 500 000 элементаў у галоўнай ніткі, ухіліваюцца ад таго, каб робіць гэта ў однам неперарваным for-цыкле, адтак як гэта заморозіць інтерфейс. Раздзеліце роботу на часткі і вярніце кантроль назад да цыклу змаганняў між часткамі, выкарыстоўваючы setTimeout(chunkHandler, 0) або requestIdleCallback(). Гэта дае браузеру можлівасць обрабатваць клікі і перамальовваць кадры між часткамі.

    Друга рэч — будзьце точныя ў тым, што на самай справе робяць async/await. Це сынтаксічны «цукор», які паверх обяваеў: любы код, напісаны до першага await, выконваецца сінхронна, тады як усё, што лежыць пасля await, запаковваецца ў мікратаск-калебак, які застаецца на паузе пакуль обява, якая чакаецца, не будзе выканана.

    Трэць, ніколы не спрыяйце таймерам як даўнейшым стопвачам. Якщо вы ствараеце ўсё такое, як таймер у самай прыемлівасці чы ўстройства для керавання аудыя, не пакладайцеся на setInterval чы setTimeout, каб досягнуць точных пазначэнняў у мілісекундах. Навантажэння на систему, обмежэнне роботы фонавых вікнаў браузерамі для захоўкі акумулятара і затрымкі ў чергах можу прывести да пакрыцчаў. У замен меркуйце прыбылы час па системнай гадзіне, выкарыстоўваючы performance.now().

    JavaScript, які работае на аднай ніті, — гэта не обмежэнне, якое трэба адмахнуцца; гэта адна з наймоцнейшых прыемаў проектавання гэтага языка. Спалучаючы аднай швэйкі сінхронны двыгун з наборам фонавых API браузера і чыста керованым цыклам падзеяў, JavaScript стварае вражанне адразу ж выконвання багато задач, у той час як ухіляецца ад ситуацый канкурэнціі, застряганняў і проблем з безпекай нітей, якія єсць у справжнім багатанітевым падходзе ў іншых языках.

    Калі вы зрозумеете і пачнёте спакою ставіцца да таго, як працуюць чергі, спосаб выканання вашага коду больш не будзе ствараць нейкіх неспакоў.