Галоўная / Артыкулы / Ядро JavaScript з адзінам вялам протыраўцем працы протыма з мнагапроцэсовым ядром прыгледача

Ядро JavaScript з адзінам вялам протыраўцем працы протыма з мнагапроцэсовым ядром прыгледача

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

1692 слоў

JavaScript выконваецца на аднай нітках.

Вы, верагаце, чыталі гэты тверджэнне больш разоў, ніж можаце памерыць.

Аднак калі вы завантажваеце сучасную веб-сторонку, вы можетэ бачыць, як адночасна відбываюцца всі наступныя рэчы:

Выпрашваецца сіткова запытка.

Зяўляецца і декодуецца зображэнне.

Плавна праўяцца анімазыя CSS.

Сторанка мгновенна рэагуе на клік.

Контэнт адрасаваецца на экран.

І ваш код JavaScript продовжае выконваліцца ўсё гэта час.

То што ж на самай справе відбываецца пад капотам?

Якшо у JavaScript є толькі адна нітка, хто ж керуе всім іншым?

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

JavaScript і прыгледчык — гэта не адна і тая ж рошчынь.

У JavaScript є галоўная нітка

Калі разработчыкі называюць JavaScript адно-нитевым, яны маюць на увазе самэ гэта, як выконваецца код на JavaScript.

Ваш код выканаўсяе на адной галоўнай ніцы JavaScript.

Возьмім гэты фрагмент як прыклад:

console.log("One");
console.log("Two");
console.log("Three");

Кожная лінія выканаўсяе строго пасля пылейшнай.

JavaScript не будзе выканаўсяць гэтыя тры запаведзі заодно на адной ніцы.

Є толькі адна стака вызываў, з якою можна працаваць.

У кожны момент часу выканаўсяе толькі адна частка коду на JavaScript.

Гэтае і ёсць сутнасць адно-нітевасці у гэтым контексте.

Але тут все стае болей складным.

Сам браузер адпаведна за гораздка больш, чым проста выканаўсяе вашыя скрыпты.

У браузера больш задач, чым толькі выканаўсяе JavaScript

Браузер — гэта набліжна большая машина, чым проста двыгун JavaScript.

Ён должен кераваць такімі задачамі, як:

  • Зялленне дадзейна па сетцы
  • Рэжымаванне таймераў
  • Зялленне дадзейна з клавіатуры і курсора
  • Стварэнне фрэмоў на экране
  • Декодаванне адпраўленых зображэнняў
  • Адпраўленне звуку
  • Кераванне адпраўленнем відеа
  • Зберагчэнне дадзейна на дыску
  • Расчытак раскладу стораніцы
  • Адрысаванне пікселей пад час адрасавання
  • Спаўненне шароў пад час складання
  • Разныя іншыя задачы, якімі керуе браузер і апэратыўная система

Ваш код на JavaScript не ёсць тым, яны безпасэродзе выканае ўсё гэта.

У замен JavaScript можа дапраўіць задачы браузеру і дазволіць яму кераваць деталямі.

Напрыклад:

fetch("/api/users");

Ваш скрапт пачынае запит.

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

Эта адміністрацыя належыць браузеру і системам, якія ў яго складзе.

Калі прыходзіць адпаведна вясноўка, браузер плануе выконанне вашага функцыі-вызову або продажэння обявленага задання на нитку JavaScript.

Гэта даскладна адрозніцца ад уявы, што:

"JavaScript керуе абсалютна всім."

Спакульвайце JavaScript як аднаго працавіка ў значна большай системе

Уявіце сабе ресторан як ментальную модель.

JavaScript — це адзін сервер, який прымае замовленні.

Браузер апрантавае всю дзейнасць ресторана.

Багато іншых працавікаў адпавядаюць за разныя задачы на тле.

JavaScript можа сказаць:

"Мне трэба гэтыя даны з сервера."

У тым моманце браузер перэймае кераванне сетевай дзейнасцю.

JavaScript не павінен зупніцца і чакаць, пакуль не прыйдзе кожны байт.

Ён можа працаваць над іншымі завданнямі.

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

Гэта ёсць аднойчынкай таго, як працуюць асінхронныя API прыгледача.

Тады што відбываецца з fetch()?

Возьмім такі прыклад:

console.log("Start");
fetch("/api/users")
  .then(() => {
    console.log("Users received");
  });
console.log("End");

Новачкі часта уявляюць сабе ўсё так прыблізна:

Start
   ↓
fetch()
   ↓
wait for server
   ↓
Users received
   ↓
End

Але гэта не ўсё так точна паказвае тое, што насправды відбываецца.

Выконанне JavaScript не зупніваецца на вызове fetch(), каб чакаць адпаведны адказ.

Болей точныя зусімцы выглядаюць так:

JavaScript
   │
   ├── Start fetch
   │
   ▼
Browser handles network work
   │
   │
   └───────────────┐
                   │
JavaScript         │
continues          │
                   │
   ▼               │
console.log("End") │
                   │
                   ▼
        Response becomes available
                   │
                   ▼
        Promise continuation
        gets scheduled
                   │
                   ▼
        JavaScript runs it

З урахоўваннем такога прайходу, выведзенне ў консоль зазвычай выглядае так:

Start
End
Users received

Ключовым моментам тут не ўсё толькі тое, што fetch() працюе асінхронна.

Найважлівейша деталь – хто на самом прыкладзе чакае.

Працэўнік JavaScript ніколі не блакуе пад час чакання на сеть.

Цыкл змаганняў спаявае всё разам

Самэлькі тут і выступае цыкл змаганняў.

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

Спрасаваная версія гэтай системы выглядае так:

Browser
                │
     ┌──────────┼───────────┐
     │          │           │
 Network     Timers      User Input
     │          │           │
     └──────────┼───────────┘
                │
                ▼
         Scheduling queues
                │
                ▼
           Event Loop
                │
                ▼
          Call Stack
                │
                ▼
           JavaScript

Памятаце, што гэтыя схемы ўсё ж спрасаваныя.

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

Протыма, гэтыя схемы адображаюць основную ідею:

Выкананне JavaScript — толькі частка значна большага системы.

Таймеры не выкананяюць ваш код у фонавым режыме

Возьмім такі прыклад:

setTimeout(() => {
  console.log("Done");
}, 1000);

Апэлляўны спосаб прыкладзіць гэта ёсць:

«JavaScript запускае адзінокы потак, які зменшае лічбу на адну секунду».

Такая канцэпцыя вводзіць вас у памылку.

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

Это значыць, што код падобны гэтаму — глупая ідея:

setTimeout(() => {
  console.log("Done");
}, 1000);

while (true) {}

Чаму гэта паслабляе работу?

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

Браузер абавесці можа знайсці, што:

«Таймер запрацаваў.»

Але толькі ведаць гэта недастатня — JavaScript все раве патрэбуе можлівасці, каб фактычна яго выканаць:

Main JavaScript thread

while (true) {
   // never finishes
}
        ↓
Timer becomes ready
        ↓
Callback waits
        ↓
JavaScript never becomes available

Гэта адна з ключоўых разлікоў, якія варта запам’ятаваць.

Асінхроннае выкананне не значыць, што ваш калебэк выкананае на іншым потоку.

Тады чаму сторана не застойваецца ўсё час?

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

Аднак існуе адна прычына, яку варта выявіць.

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

Самэй таго код у такім формате все равно можа заблокаваць сторанку:

const start = performance.now();

while (performance.now() - start < 5000) {
  // expensive work
}

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

Самэй таго людзі кажу:

"JavaScript працюе на аднам адгуку."

Канкурэнцыя на рэвэрсе браузера, аднадгуковасць у JavaScript

Гэтае ўжо важлівая разліка, якую трэба памяцать.

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

У загальных рысах задачі можна распадзіць так:

Browser
│
├── JavaScript execution
├── Network activity
├── Rendering-related work
├── Image/media processing
├── Browser services
└── Other internal tasks

Аднак нічыя з гэтых магчымас не дае можлівасці напісаць ў такім формате:

runThisFunctionOnAnotherBrowserThread();

і ўбедзіцца, што будь-які код на JavaScript перейдзе на іншы калькольнік. Звычны код на JavaScript продовжае выконваліцца за моделлю адносульковага выканання, якая у ўсі часы была у нём. Якщо вам спецыяльна трэба перанесці трыудзеллівыя вычысленні ў кодзе на JavaScript з галоўнага калькольніка, для гэтага і існуюць Web Workers.

Вывык

Адносульковы JavaScript не значыць, што браузер у цэлы зводзіцца да аднаго калькольніка.

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

Такія речы, як запыты да сеті, таймеры, обробка вводу ад корыстніка, атрыбутаванне і декодаванне медыя-контэнту, можаць выконваны незалежна ад выконвання вашага JavaScript.

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

Пры тым галоўная нітка ўсё ж такі несе вялікую навантажэння.

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

Такім чынам мы маем браузер, які ў цэлым є дасягнутым у сваіх дзеяннях, але выконванне JavaScript застаецца строго на аднай ніткай.

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

Ключовыя выводы

Запамятавайце гэтыя тры моменты:

  1. Сам JavaScript ўжо є однапраменны. Ваш код выконваецца парадульна, шаг за шагам, на галоўнай прамені.
  2. Браузер – гэта большая, канкурентная среда. Перад тым, як запусціць ваш JavaScript, ён паралельна керуе сетевымі зв’язкамі, таймерамі, атрыбутамі, вводам і обробкай медыя-матэрыялу.
  3. Асінхронна праця не значыць мнагапраменнага выканання вашага калебаку. Браузер сам чакае, а потым вяртае кантроль JavaScript, калі на галоўнай прамені з’являецца вольнае месца.

Прыдатны спосаб прыкладзення гэтага падходу:

Browser
│
├── JavaScript execution
├── Network activity
├── Timers
├── User input
├── Rendering
├── Media processing
└── Other browser systems

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

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

Супаўзвязаныя матэрыялы