🎢 Event loop: неочевидные нюансы
Большинство из нас знает, что event loop выполняет сначала одну макротаску, затем все микротаски, после чего наконец может сделать отрисовку. Однако на практике многие путаются. Расскажу о нюансах, в которых многие ошибаются.
1️⃣ Тело промиса — это синхронный код
Функция, которую мы передаем в конструктор промиса, она же promise executor, является синхронным кодом и выполняется в общем потоке синхронного кода.
console.log('before');
new Promise((res) => {
console.log('promise'); // sync
res();
}).then(() => console.log('then')); // async
console.log('after');
// => before, promise, after, then
В этом случае мы увидим promise между before и after, потому что это синхронный код. А вот колбек в then — это микротаска и поэтому она выполнится тогда, когда освободится колстек от текущей задачи.
2️⃣ После макротаски выполняются все микротаски. Но только те, которые поставлены в очередь к этому моменту!
setTimeout(() => console.log('setTimeout')); // макротаска 1
new Promise((resolve) => {
console.log('promise');
setTimeout(resolve); // макротаска 2
}).then(() => console.log('then')); // микротаска
// => promise, setTimeout, then
В этом случае макротаска 1 выполнится до микротаски — потому что микротаска будет поставлена в очередь только макротаской 2.
3️⃣ Resolve промиса не означает прекращение выполнение функции-экзекьютора
new Promise((resolve) => {
resolve();
console.log('promise');
}).then(() => console.log('then'));
// => promise, then
После вызова resolve() функция-экзекьютор будет продолжать свое выполнение, resolve не работает как return
4️⃣ Async/await — это синтаксический сахар над promise
А значит, async работает примерно как promise executor, который, как мы выяснили, вполне себе синхронный. Поэтому если вы встречаете вызов асинхронной функции и затем синхронный код, то первая начнет выполняться раньше (и сделает паузу на await)
const delay = new Promise((res) => setTimeout(res));
async function f1() {
console.log('async function');
await delay;
}
f1();
console.log('sync code');
Здесь f1 попадает в колстек раньше console.log('sync code’), и пускай вас не смущает слово async — все, что до await, вполне себе синхронно
5️⃣ А вот await создает микротаску
Которая выполнится только после того, как завершится промис, которого она ждет. А вот другой код не ждет и поэтому, если ваша функция поделена await-ом, то между частями кода (до await и после) может успеть выполниться какой-нибудь другой код, который может изменить состояние приложения. Поэтому, если вы опираетесь на внешнее состояние (данные из localStorage, например), то оно может быть разным до и после await.
const delay = new Promise((res) => setTimeout(res));
localStorage.setItem('value', 1);
async function f1() {
const value1 = localStorage.getItem('value'); // 1
console.log('async function', value1);
await delay;
const value2 = localStorage.getItem('value'); // 2
console.log('after await', value2);
}
f1();
console.log('sync code');
localStorage.setItem('value', 2);
🔜 Канал в MAX
#event_loop
Большинство из нас знает, что event loop выполняет сначала одну макротаску, затем все микротаски, после чего наконец может сделать отрисовку. Однако на практике многие путаются. Расскажу о нюансах, в которых многие ошибаются.
1️⃣ Тело промиса — это синхронный код
Функция, которую мы передаем в конструктор промиса, она же promise executor, является синхронным кодом и выполняется в общем потоке синхронного кода.
console.log('before');
new Promise((res) => {
console.log('promise'); // sync
res();
}).then(() => console.log('then')); // async
console.log('after');
// => before, promise, after, then
В этом случае мы увидим promise между before и after, потому что это синхронный код. А вот колбек в then — это микротаска и поэтому она выполнится тогда, когда освободится колстек от текущей задачи.
2️⃣ После макротаски выполняются все микротаски. Но только те, которые поставлены в очередь к этому моменту!
setTimeout(() => console.log('setTimeout')); // макротаска 1
new Promise((resolve) => {
console.log('promise');
setTimeout(resolve); // макротаска 2
}).then(() => console.log('then')); // микротаска
// => promise, setTimeout, then
В этом случае макротаска 1 выполнится до микротаски — потому что микротаска будет поставлена в очередь только макротаской 2.
3️⃣ Resolve промиса не означает прекращение выполнение функции-экзекьютора
new Promise((resolve) => {
resolve();
console.log('promise');
}).then(() => console.log('then'));
// => promise, then
После вызова resolve() функция-экзекьютор будет продолжать свое выполнение, resolve не работает как return
4️⃣ Async/await — это синтаксический сахар над promise
А значит, async работает примерно как promise executor, который, как мы выяснили, вполне себе синхронный. Поэтому если вы встречаете вызов асинхронной функции и затем синхронный код, то первая начнет выполняться раньше (и сделает паузу на await)
const delay = new Promise((res) => setTimeout(res));
async function f1() {
console.log('async function');
await delay;
}
f1();
console.log('sync code');
Здесь f1 попадает в колстек раньше console.log('sync code’), и пускай вас не смущает слово async — все, что до await, вполне себе синхронно
5️⃣ А вот await создает микротаску
Которая выполнится только после того, как завершится промис, которого она ждет. А вот другой код не ждет и поэтому, если ваша функция поделена await-ом, то между частями кода (до await и после) может успеть выполниться какой-нибудь другой код, который может изменить состояние приложения. Поэтому, если вы опираетесь на внешнее состояние (данные из localStorage, например), то оно может быть разным до и после await.
const delay = new Promise((res) => setTimeout(res));
localStorage.setItem('value', 1);
async function f1() {
const value1 = localStorage.getItem('value'); // 1
console.log('async function', value1);
await delay;
const value2 = localStorage.getItem('value'); // 2
console.log('after await', value2);
}
f1();
console.log('sync code');
localStorage.setItem('value', 2);
🔜 Канал в MAX
#event_loop