Разница между вызовом события из браузера и из кода

Рейтинг: 2Ответов: 1Опубликовано: 11.05.2023

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

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

const button1 = document.querySelector('#button1');
const button2 = document.querySelector('#button2');

button1.addEventListener('click', () => {
  console.clear();
  
  Promise.resolve().then(() => console.log('Microtask 1'));
  console.log('Task 1');
});

button1.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 2'));
  console.log('Task 2');
});

button2.addEventListener('click', () => button1.click());
<button id="button1">Click to trigger "click" event</button>
<button id="button2">Click to see how JS triggers the "click" event</button>

Почему при вызове события при нажатии на кнопку физически порядок таков Task 1, Microtask 1, Task 2, Microtask 2, а при вызове события из кода порядок таков Task 1, Task 2, Microtask 1, Microtask 2?

Ответы

▲ 3

Думаю из содержания выводимых сообщений опытные разработчики уже поняли в чём дело :)


Перед тем как дальше читать важно знать:

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

    Например промис-ы используют очередь микрозадач для выполнения своих колбэк-функций

  • Новая задача кладётся в стэк выполнения, только после того как отрабатывают все микрозадачи предыдущей задачи. Потому если какая-та задача или микрозадача генерирует бесконечное количество микрозадач (например с помощью queueMicrotask), то следующая задача не выполнится никогда


Про объявление переменных рассказывать не буду, т.к. к сути вопроса не имеет отношения. Также не буду рассказывать про слушателя кнопки button2 т.к. он просто нужен чтобы в желаемое время вызвать button1.click() и console.clear() т.к. он просто нужен чтобы не засорять консоль

Общее для обоих методов:

  1. button1.addEventListener(/*1*/) - добавляется первый слушатель на кнопку button1

  2. button1.addEventListener(/*2*/) - добавляется второй слушатель на кнопку button1

Клик на кнопку физически

  1. Вызывается первый слушатель:

    1.1. Вызывается первая колбэк-функция - в стэк выполнения ставится первая колбэк-функция

    1.2. Promise.resolve().then(() => console.log('Microtask 1')) - в очередь микрозадач ставится новая микрозадача console.log('Microtask 1')

    1.3. console.log('Task 1') - в стэк выполнения ставится новая задача

    1.4. Т.к. в первой колбэк-функции больше ничего нет, то в стэке выполняется, то что там лежит в обратном порядке. Последнее что мы туда положили - console.log('Task 1'), а значит он и выполнится и мы в консоли увидим Task 1. После выполнения она убирается из стэка выполнения

    1.5. Теперь в стэке осталась первая колбэк-функция, но т.к. ей больше нечего делать, то она заканчивает свою работу и убирается из стэка выполнения

    1.6. Наконец стэк выполнения становится пустым и начинают выполняться микрозадачи. Там одна микрозадача - console.log('Microtask 1'), после его выполнения мы увидим Microtask 1

  2. Т.к. теперь очередь микрозадач пустой, то вызывается второй слушатель:

    2.1. Вызывается вторая колбэк-функция - в стэк выполнения ставится вторая колбэк-функция

    2.2. Promise.resolve().then(() => console.log('Microtask 2')) - в очередь микрозадач ставится новая микрозадача console.log('Microtask 2')

    2.3. console.log('Task 2') - в стэк выполнения ставится новая задача

    2.4. Т.к. в первой колбэк-функции больше ничего нет, то в стэке выполняется, то что там лежит в обратном порядке. Последнее что мы туда положили - console.log('Task 2'), а значит он и выполнится и мы в консоли увидим Task 2. После выполнения она убирается из стэка выполнения

    2.5. Теперь в стэке осталась первая колбэк-функция, но т.к. ей больше нечего делать, то она заканчивает свою работу и убирается из стэка выполнения

    2.6. Наконец стэк выполнения становится пустым и начинают выполняться микрозадачи. Там одна микрозадача - console.log('Microtask 2'), после его выполнения мы увидим Microtask 2

Теперь мы явно увидели почему порядок вывода при клике физически таков Task 1, Microtask 1, Task 2, Microtask 2

Клик на кнопку из кода

  1. Вызывается button1.click() - в стэк выполнения ставится button1.click()

  2. Вызывается первый слушатель:

    2.1. Вызывается первая колбэк-функция - в стэк выполнения ставится первая колбэк-функция

    2.2. Promise.resolve().then(() => console.log('Microtask 1')) - в очередь микрозадач ставится новая микрозадача console.log('Microtask 1')

    2.3. console.log('Task 1') - в стэк выполнения ставится новая задача

    2.4. Т.к. в первой колбэк-функции больше ничего нет, то в стэке выполняется, то что там лежит в обратном порядке. Последнее что мы туда положили - console.log('Task 1'), а значит он и выполнится и мы в консоли увидим Task 1. После выполнения она убирается из стэка выполнения

    2.5. Далее на очереди первая колбэк-функция, но т.к. ей больше нечего делать, то она заканчивает свою работу и убирается из стэка выполнения

    2.6. И вот ключевой момент - наш стэк выполнения не пуст, там всё ещё осталась невыполненная функция button1.click(). Когда очередь доходит до него, то он продолжает свою работу

  3. Вызывается второй слушатель:

    2.1. Вызывается вторая колбэк-функция - в стэк выполнения ставится вторая колбэк-функция

    2.2. Promise.resolve().then(() => console.log('Microtask 2')) - в очередь микрозадач ставится новая микрозадача console.log('Microtask 2'). Теперь у нас в очереди микрозадач 2 микрозадачи - console.log('Microtask 1') и console.log('Microtask 2')

    2.3. console.log('Task 2') - в стэк выполнения ставится новая задача

    2.4. Т.к. в первой колбэк-функции больше ничего нет, то в стэке выполняется, то что там лежит в обратном порядке. Последнее что мы туда положили - console.log('Task 2'), а значит он и выполнится и мы в консоли увидим Task 2. После выполнения она убирается из стэка выполнения

    2.5. Теперь в стэке осталась первая колбэк-функция, но т.к. ей больше нечего делать, то она заканчивает свою работу и убирается из стэка выполнения

    2.6. Очередь доходит до button1.click() и вот теперь то он и завершает свою работу, вызвав всех слушателей и убирается из стэка выполнения

    2.7. Наконец стэк выполнения становится пустым и начинают выполняться микрозадачи. Т.к. это очередь, а не стэк, то выполняются микрозадачи по очереди. Первым на очереди у нас console.log('Microtask 1'), а значит мы увидим Microtask 1

    2.8. Вторым на очереди у нас console.log('Microtask 2'), а значит мы увидим Microtask 2

Теперь мы явно увидели почему порядок вывода при клике на кнопку из кода таков Task 1, Task 2, Microtask 1, Microtask 2