Разница между вызовом события из браузера и из кода
До недавного времени я думал, что при вызове события из кода, происходит ровно то же самое что и при вызове события из браузера. Оказалось это вообще не так, просто в большинстве случаев мы не сталкиваемся с этой разницей
Для примера запустите код ниже и внимательно смотрите за очередью вывода сообщений в консоли:
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?
Ответы (1 шт):
Думаю из содержания выводимых сообщений опытные разработчики уже поняли в чём дело :)
Перед тем как дальше читать важно знать:
Микрозадачи выполняются после выхода из создавшей их функции или программы и только в том случае, если стэк выполнения JavaScript пуст, но перед возвратом управления циклу событий, используемому пользовательским агентом для управления средой выполнения сценария
Например промис-ы используют очередь микрозадач для выполнения своих колбэк-функций
Новая задача кладётся в стэк выполнения, только после того как отрабатывают все микрозадачи предыдущей задачи. Потому если какая-та задача или микрозадача генерирует бесконечное количество микрозадач (например с помощью queueMicrotask), то следующая задача не выполнится никогда
Про объявление переменных рассказывать не буду, т.к. к сути вопроса не имеет отношения. Также не буду рассказывать про слушателя кнопки button2 т.к. он просто нужен чтобы в желаемое время вызвать button1.click() и console.clear() т.к. он просто нужен чтобы не засорять консоль
Общее для обоих методов:
button1.addEventListener(/*1*/)- добавляется первый слушатель на кнопкуbutton1button1.addEventListener(/*2*/)- добавляется второй слушатель на кнопкуbutton1
Клик на кнопку физически
Вызывается первый слушатель:
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.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
Клик на кнопку из кода
Вызывается
button1.click()- в стэк выполнения ставитсяbutton1.click()Вызывается первый слушатель:
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(). Когда очередь доходит до него, то он продолжает свою работуВызывается второй слушатель:
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 12.8. Вторым на очереди у нас
console.log('Microtask 2'), а значит мы увидимMicrotask 2
Теперь мы явно увидели почему порядок вывода при клике на кнопку из кода таков Task 1, Task 2, Microtask 1, Microtask 2