Галоўная > Браўзерны Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (1 частка)

Браўзерны Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (1 частка)

js
Event
loop

Пераклад артыкула Browser Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (1 частка)

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

Browser Event loop

Event loop

Старыя аперацыйныя сістэмы не падтрымлівалі шматструменнасць (multithreading), і іх цыкл падзей можна прыблізна апісаць як просты цыкл:

while (true) {
    if (execQueue.isNotEmpty()) {
        execQueue.pop().exec();
    }
}

Гэты код выкарыстоўвае ўвесь CPU. Так было ў старых аперацыйных сістэмах. Сучасныя планавальнікі аперацыйных сістэм вельмі складаныя. У іх ёсць прыярытэты, чэргі выканання і многія іншыя тэхналогіі.

Мы можам пачаць апісваць цыкл падзей як цыкл, які правярае, ці ёсць у нас невыкананыя задачы:

pending tasks

Каб атрымаць заданне для выканання, давайце вызначым

✍️ спіс трыгераў, якія могуць змясціць задачу ў цыкл падзей:

  1. Тэг <script>
  2. Адкладзеныя задачы: setTimeout, setInterval, requestIdleCallback
  3. Апрацоўшчыкі падзей з браўзернага API click, mousedown, input, blur і гэтак далей
    • Некаторыя падзеі ініцыююцца карыстальнікам, напрыклад, клікі мышы, пераключэнне ўкладак і г.д.
    • Некаторыя з іх мы выкарыстоўваем у нашым кодзе: апрацоўшчык адказу XmlHttpRequest, выкананы fetch і г.д.
  4. Змены стану promise. Падрабязней пра абяцанні ў маёй серыі
  5. Такія назіральнікі, як DOMMutationObserver, IntersectionObserver
  6. RequestAnimationFrame

Амаль усё, што мы апісалі вышэй, плануецца праз WebAPI (або browserAPI).

Напрыклад, у нашым кодзе ёсць такі радок:

setTimeout(function a() {}, 100)

Калі мы выконваем setTimeout, WebAPI адкладае задачу на 100 мс. Праз 100 мс WebAPI ставіць function a у чаргу (думаю тут абдрукоўка. У арыгінале - function a(). Але ж мы не можам дадаць выклік функцыі ў чаргу). Мы можам назваць гэта TaskQueue. EventLoop атрымлівае задачу на наступнай ітэрацыі цыклу і выконвае яе.

Мы абмяркавалі задачы ў нашым цыкле падзей. Наш JS-код і браўзер павінны мець магчымасць працаваць з DOM.

Наш js-код:

  • Чытае даныя элементаў DOM: памер (size), атрыбуты, становішча (position) і г.д.
  • Змяняе атрыбуты: data- attr, шырыня (width), вышыня (height), становішча (position), уласцівасці CSS і г.д.
  • Стварае / выдаляе HTML-вузлы (nodes).

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

✍️ Сучасныя браўзеры выконваюць і JS, і паток візуалізацыі ў адным патоку (за выключэннем выпадкаў, калі мы ствараем Web/Shared/Service Worker).

Гэта азначае, што EventLoop павінен мець 'рэндэрынг' ('rendering') у схеме. Паток рэндэрынгу - гэта не адна аперацыя. Я б сказаў, што гэта чарга візуалізацый (render queue):

render queue

Цяпер у нас ёсць 2 крыніцы задач для выканання EventLoop. Першы - гэта RenderQueue, а другі - 'SomeJsTasks'. Ці павінен браўзер выбраць 1 задачу з чаргі задач js, а затым візуалізаваць старонку? Каб адказаць на пытанне, давайце паглядзім на праблему абнаўлення экрана:

Абнаўленне экрана

Для браўзераў цыкл падзей звязаны з кадрамі (frames), паколькі EventLoop выконвае і код JS, і адлюстроўвае старонку. Я б прапанаваў разглядаць кадр (frame) як адзіны здымак стану экрана, які карыстальнік бачыць праз імгненне.

✍️ Браўзеры імкнуцца паказаць абнаўленні на старонцы як мага хутчэй, улічваючы існуючыя абмежаванні ў апаратным і праграмным забеспячэнні:

  • Апаратныя абмежаванні: частата абнаўлення экрана
  • Праграмныя абмежаванні: налады АС (аперацыйная сістэма), браўзер і яго налады, налады энергазберажэння і інш.

✍️ Пераважная большасць браўзераў / АС падтрымлівае 60 FPS (кадраў у секунду). Браўзеры спрабуюць абнавіць экран з такой хуткасцю.

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

Гэта азначае, што браўзеры маюць часовыя інтэрвалы ў 16.6 мс (1000/60) для выканання задач, перш чым яны павінны візуалізаваць новы кадр (а візуалізацыя новага кадра таксама зойме час).

Task queue і Micro Task Queue

Цяпер прыйшоў час раскласці 'SomeJsTasks' і зразумець, як гэта працуе.

Браўзэры выкарыстоўваюць 2 чэргі для выканання нашага кода:

  1. Task Queue або Macro Task Queue прысвечаны ўсім падзеям, адкладзеным задачам і г.д.
  2. Micro Task Queue прызначана для зваротных выклікаў promises: як вырашаных, так і адхіленых, а таксама для MutationObserver. Адзіны элемент з гэтай чаргі - Micro Task.

Зараз давайце паглядзім на іх абодвух:

Task queue (чарга задач)

Калі браўзер атрымлівае новую задачу, яна змяшчае яе ў Task Queue. Кожны цыкл Event Loop бярэ задачу з Task Queue і выконвае яе. Пасля выканання задачы, калі ў браўзера ёсць час (у чарзе візуалізацыі няма задач), Event Loop атрымлівае яшчэ адну задачу з Task Queue і яшчэ адну задачу, пакуль чарга візуалізацыі не атрымае задачу для выканання.

Першы прыклад:

Task Queue

У нас ёсць 3 задачы: A, B, C. Event Loop атрымлівае першую і выконвае яе. Гэта займае 4 мс. Затым цыкл падзей правярае іншыя чэргі (чаргу мікразадач ( Macro Task Queue) і чаргу візуалізацыі (Render Queue)). Яны пустыя. Цыкл падзей выконвае задачу B. Гэта займае 12 мс. У суме дзве задачы выкарыстоўваюць 16 мс. Затым браўзер дадае задачы ў чаргу візуалізацыі, каб намаляваць новы кадр. Цыкл падзей правярае чаргу візуалізацыі і запускае выкананне задач у чарзе візуалізацыі. Яны займаюць каля 1 мс. Пасля гэтых аперацый цыкл падзей вяртаецца ў Task Queue і выконвае апошнюю задачу C.

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

✍️ Падчас выканання кода JS усе змены, унесеныя JS, не будуць прадстаўлены карыстальніку ў выглядзе візуалізаванага фрэйма, пакуль не будуць выкананыя макразадачы і ўсе незавершаныя мікразадачы. Аднак код JS можа вылічыць змены DOM.

Другі прыклад:

render queue

У нас у чарзе толькі 2 задачы (A, B). Першая задача А займае 240 мс. Паколькі 60 кадраў у секунду азначае, што кожны кадр павінен візуалізавацца кожныя 16,6 мс, браўзер губляе прыкладна 14 кадраў. Калі задача A скончваецца, цыкл падзей выконвае задачы з чаргі візуалізацыі, каб намаляваць новы кадр. Важная заўвага: нават калі мы страцілі 14 кадраў, гэта не значыць, што мы будзем адлюстроўваць 15 кадраў запар. Гэта будзе адзіны кадр.

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

Call Stack (стэк выклікаў)

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

Давайце паглядзім на прыкладзе:

function findJinny() {
  debugger;
  console.log('Dialog with Jinny');
}

function goToTheCave() {
  findJinny();
}

function becomeAPrince() {
  goToTheCave();  
}

function findAFriend() {
   // ¯\_(ツ)_/¯
}

function startDndGame() {
  const friends = [];
  while (friends.length < 2) {
    friends.push(findAFriend());
  }
  becomeAPrince();
}
console.log(startDndGame());

Гэты код будзе прыпынены інструкцыяй debugger.

Мы пачынаем наш стэк з убудаванага кода:

console.log(startDndGame());

Гэта пачатак стэка выклікаў. Як правіла, Сhrome паказвае спасылку на гэты радок. Давайце пазначым гэта як inline. Затым мы пераходзім да функцыі startDndGame і findAFriend, якая выклікаецца некалькі разоў. Гэтая функцыя не будзе прадстаўлена ў стэку выклікаў, бо яна завяршаецца да таго, як мы пяройдзем да адладчыка (debugger). Вось як выглядае стэк выклікаў, калі мы спыняемся на адладчыку:

call stack

✍️ Калі стэк выклікаў апусцее, бягучая задача выканана.

Што такое мікразадачы (Microtasks)?

Ёсць толькі 2 магчымыя крыніцы мікразадач: зваротныя выклікі (callbacks) Promise (onResolved/onRejected) і зваротныя выклікі MutationObserver.

Мікразадачы маюць адну галоўную асаблівасць, якая робіць іх цалкам рознымі:

✍️ Мікразадача будзе выканана, як толькі стэк выклікаў стане пустым.

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

Давайце паглядзім на прыклад, дзе ў чарзе мікразадач ёсць 4 мікразадачы:

microtask queue

Першая мікразадача, якую трэба выканаць, - A. A займае 200 мс, і ў нас ёсць задачы ў чарзе візуалізацыі. Аднак яны будуць адкладзены, таму што ў чарзе мікразадач яшчэ ёсць 3 задачы. Гэта азначае, што пасля цыкла падзей A выконваюцца мікразадачы B, C і, нарэшце, D. Калі чарга мікразадач пустая, цыкл падзей адлюстроўвае новы кадр. У прыкладзе выкананне гэтых 4 мікразадач займае 0,5 секунды. Увесь гэты час карыстацкі інтэрфейс браўзера быў заблакаваны і не інтэрактыўны.

✍️ Наступныя мікразадачы могуць заблакіраваць карыстацкі інтэрфейс вэб-сайта і зрабіць старонку неінтэрактыўнай.

Гэтая асаблівасць мікразадач можа быць як перавагай, так і недахопам. Напрыклад, калі MutationObserver выклікае свой callback у адпаведнасці са зменамі DOM, карыстальнік не ўбачыць змяненняў на старонцы да завяршэння працы callback. Такім чынам, мы можам эфектыўна кіраваць кантэнтам, які бачыць карыстальнік.

Абноўленая схема цыкла падзей:

updated event loop

Працяг тут.

loveJS, 2023-06-27
Каментары

    (Каб даслаць каментар залагуйцеся ў свой уліковы запіс)

    ;