Skip to content

event loop

garevna edited this page Aug 1, 2019 · 10 revisions

🎓 Event Loop

Событийно-ориентированное программирование ( Event-Driven Programming ) — это когда выполнение кода детерминируется событиями

Основой Event-Driven Programming является Event Loop, регулирующий поток событий

🔗 MDN

При запуске нового скрипта движок JS:

1) принимает содержимое входного файла
2) оборачивает это в функцию
3) устанавливает эту функцию обработчиком события «запуск» программы
4) инициализирует другие переменные и функции
5) эмитирует событие запуска программы
6) добавляет это событие в очередь событий
7) извлекает это событие из очереди и выполняет зарегистрированный обработчик,
и, наконец! -
8) наша программа работает!

Asynchronous Programming in Javascript CSCI 5828:
Foundations of Software Engineering Lectures 18–10/20/2016
by Kenneth M. Anderson

⚠️ Ничто не исполнится, не попав в Call Stack
⚠️ Все, что исполняется, пришло из Task Queue
⚠️ Все, что находится в Task Queue, называется task


🎓 Поток выполнения ( thread )

Поток — это последовательность команд ( операторов языка, вызовов функций и т.д. ), которые выполняются последовательно друг за другом

Несколько потоков могут существовать в рамках одного и того же процесса и совместно использовать ресурсы ( память )

Процесс — это экземпляр исполняемой программы, которому выделены системные ресурсы ( время процессора и память)
Каждый процесс выполняется в отдельном адресном пространстве
Один процесс не может получить доступ к данным другого процесса

Каждая программа создает по меньшей мере один основной поток, который запускает функцию main()

Программа, использующая только основной поток, является однопоточной

Многопоточные языки используют несколько потоков

При наличии только одного процессора ему приходится довольно часто переключаться с одного потока на другой, чтобы создать иллюзию одновременного выполнения кода во всех потоках
В многопроцессорных и многоядерных системах каждый процессор ( ядро ) обслуживает отдельный поток, поэтому потоки действительно выполняются параллельно ( одновременно )


🎓 Стек и куча ( heap )

Стек — это "быстрый" кусок оперативной памяти

Стек создаётся для каждого потока в многопоточных языках
JS однопоточный язык, поэтому у нас только один стек ( Call Stack )
Стек организован по принципу LIFO ( первым пришел - последним ушел )
Размер стека ограничен
Он задаётся при создании потока
Переменные, находящиеся в стеке, всегда являются локальными ( приватными )

Heap ( "куча" ) — это оперативная память, где хранятся глобальные переменные

"Куча" допускает динамическое выделение памяти
Доступ к данным, хранящимся в "куче", обеспечивается посредством ссылок - переменных, значения которых являются адресами других переменных
Поэтому "куча" работает медленнее, чем стек
Процессор не контролирует "кучу" ( в отличие от стека ), поэтому для освобождения памяти "кучи" от ненужных переменных требуются "сборщики мусора"


🎓 Асинхронность

JS — однопоточный язык

Однако практически все движки — многопоточные

Неблокирующее поведение JS обеспечивается движком с помощью механизма Event Loop

Event Loop - это бесконечный цикл выполнения задач движком

Это событийно-ориентированная модель
Она держится на колбэках - функциях обратного вызова
Каждый колбэк связан с определенным событием

В браузере есть API, обеспечивающие
• работу таймеров ( setTimeout, setInterval )
• выполнение операций AJAX
• отслеживание событий UI

Все это происходит в параллельных потоках

Когда истекает временной интервал таймера, или завершается операция AJAX, или происходит событие UI,
вызов соответствующего колбэка помещается в очередь задач
В стек вызовов ( Call Stack ) основного потока он может попасть только отсюда,
и только тогда, когда стек вызовов будет пуст,
т.е. все текущие вызовы основного потока завершатся

Итак, движок имеет очередь задач ( Task Queue )

Если событий не происходит, эта очередь пуста

В момент, когда происходит событие, его обработчик помещается в конец очереди


1

В следующем коде есть только одна синхронная операция - это вызов функции message

message ( 'start' )

Далее в скрипте запускается множество асинхронных операций setTimeout и fetch

Расставив breakpoints, можно отследить процесс работы Event Loop

Все асинхронные операции передаются в параллельный поток движка

Что там происходит:

  • если это таймер, то отсчет временного интервала
  • если fetch, то выполняется запрос на сервер

Когда истечет временной интервал таймера или завершится fetch,

должны быть запущены соответствующие функции обратного вызова ( колбэки )

Однако они должны быть вызваны в основном процессе JS

их вызовы нужно туда "впихнуть"

но сделать это можно только при условии, что сейчас основной поток ничем не занят,

т.е. стек вызовов пуст

поэтому вызовы колбэков помещаются в очередь задач движка,

причем в том порядке, в каком их "выпихивали" в эту очередь API

теперь, как только стек вызовов будет свободен,

в него будут по очереди помещены вызовы ждущих в очереди обработчиков

function message ( text ) {
    document.body.innerText += `${text}\n\n`
}

message ( 'start' )

setTimeout ( () => message ( 'timeout 0' ) , 0 )

fetch ( "https://api.github.com/users" )
        .then (
            response => response.json ()
                .then (
                    users => message ( `1: ${users[0].login}` ) 
                )
        )

setTimeout ( () => message ( 'timeout 1' ), 0 )

fetch ( "https://api.github.com/users?since=250" )
        .then (
            response => response.json ()
                .then (
                    users => message ( `2: ${users[0].login}` )
                )
        )

setTimeout ( () => message ( 'timeout 2' ), 0 )

fetch ( "https://api.github.com/users?since=300" )
        .then (
            response => response.json ()
                .then (
                    users => message ( `3: ${users[0].login}` )
                )
        )

setTimeout ( () => message ( 'timeout 3' ), 0 )

⚠️ Таски блокируют перерисовку страницы браузером

"Тяжелый" таск может серьезно задержать выполнение других задач в очереди, которые имели несчастье попасть туда после него...

2

function message ( text ) {
    document.body.innerHTML += `<small>${text}</small><br>`
}

let start = new Date().getTime()

setTimeout (
    () => message ( `Timer real time: ${new Date().getTime()-start} ms` ),
    0
)

for ( var x=0; x<1000000000; x++ ) continue

message ( "Loop 'for' finished" )

В этом примере таймер был установлен на задержку 0 сек


3

В этом примере таск, который запускается таймером, "подвешивает" работу браузера

Обработчики события click ждут в очереди задач завершения работы таска, запущенного таймером

Если браузер не успел выполнить перерисовку страницы с сообщением "Start" до того, как запустился таск таймера, то мы увидим это сообщение на странице после того, как длинный цикл в таймере завершится...

function message ( text ) {
    document.body.innerText += `${text}\n\n`
}

document.body.onclick = ( 
    () => {
        let counter = 0
        return event => message ( `body clicked ${++counter} times` )
    }
)()

setTimeout (
    function () {
        for ( var x=0; x<10000000000; x++ ) continue
        message ( "Counting finished" )
    },
    0
)

message ( "Start" )

🎓 Microtask

Мы уже поняли, что установка таймеров и обработчиков событий UI - это создание тасков

Однако не все задачи равны "по рангу"

Есть макрозадачи и микрозадачи

У них разные очереди, т.е. они никогда не смешиваются

На каждом "витке" Event Loop выполняется одна макрозадача из очереди

После завершения очередной макрозадачи Event Loop "принимается" за очередь микрозадач

Следующий таск из очереди макрозадач не начнет выполняться до тех пор, пока не будут выполнены все микротаски из очереди микрозадач

⚠️ Перерисовка ( рендер ) страницы браузером так же не будет выполняться до тех пор, пока не будет опустошена очередь микрозадач

К числу микрозадач относятся колбэки промисов и MutationObserver


🎓 Promise

⚠️ Колбэк, передаваемый методу then() промиса, является microtask

Отличие microtask от task ( task === macrotask ) заключается в том, что браузер "впихнет" его между тасками без очереди

Иными ( правильными ) словами, следующий task ( macrotask ) из очереди не будет запущен до тех пор, пока не опустошится очередь microtask-ов


Как только мы запускаем скрипт на исполнение, мы стартуем task
В следующем примере это вызов функции message, установка таймеров и обработчика события click на document.body
Кроме того, мы запускаем три асинхронных операции, используя Fetch API браузера
Но мы знаем, что метод fetch() возвращает промис...

Т.е. как только будет получен ответ сервера, колбэк, который мы передали методу then() промиса, будет запущен сразу после завершения работы текущего таска, "подвинув" следующий за ним таск, даже если тот встал в очередь намного раньше

4

function message ( text ) {
    document.body.innerText += `${text}\n\n`
}

document.body.onclick = ( 
    () => {
        let counter = 0
        return event => message ( `body clicked ${++counter} times` )
    }
)()

message ( 'start' )

setTimeout ( () => message ( 'timeout 0' ) , 3000 )

fetch ( "https://api.github.com/users" )
    .then ( response => response.json () )
    .then ( users => message ( `1: ${users[0].login}` ) )

setTimeout ( () => message ( 'timeout 1' ), 2000 )

fetch ( "https://api.github.com/users?since=250" )
    .then ( response => response.json () )
    .then ( users => message ( `2: ${users[0].login}` ) )

setTimeout ( () => message ( 'timeout 2' ), 100 )

fetch ( "https://api.github.com/users?since=300" )
    .then ( response => response.json () )
    .then ( users => message ( `3: ${users[0].login}` ) )

setTimeout ( () => message ( 'timeout 3' ), 0 )

section.dispatchEvent ( new Event ( "click" ) )

Каждый клик будет порождать новый таск, который будет помещен в очередь задач
Если ваш клик опередит завершение операции fetch, то соответствующее сообщение появится раньше, чем имена юзеров гитхаба
Однако колбэки таймеров будут сдвинуты в очереди задач, как только завершится fetch-запрос к серверу


© Irina H.Fylyppova 2018
Использование данных материалов или любой их части коммерческими школами ( курсами ) является нарушением авторских прав


Новая версия


1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19

Занятие 1

⤵️

Занятие 2

⤴️ ⤵️

Занятие 3

⤴️ ⤵️

Занятие 4

⤴️ ⤵️

Занятие 5

⤴️ ⤵️

Занятие 6

⤴️ ⤵️

Занятие 7

⤴️ ⤵️

Занятие 8

⤴️ ⤵️

Занятие 9

⤴️ ⤵️

Занятие 10

⤴️ ⤵️

Занятие 11

⤴️ ⤵️

Занятие 12

⤴️ ⤵️

Занятие 13

⤴️ ⤵️

Занятие 14

⤴️ ⤵️

Занятие 15

⤴️ ⤵️

Занятие 16

⤴️ ⤵️

Занятие 17

⤴️ ⤵️

Занятие 18

⤴️ ⤵️

Занятие 19

⤴️ ⤵️

⤴️

ico20 Дополнительно
dir-20 Справочная инфо

Clone this wiki locally