Хабрахабр

[Перевод] Разбираемся с асинхронностью в JavaScript [Перевод статьи Sukhjinder Arora]

Привет, Хабр! Представляю вашему вниманию перевод статьи «Understanding Asynchronous JavaScript» автора Sukhjinder Arora.

Если статья вам помогла, то не поленитесь и поблагодарите автора оригинала.
От автора перевода: Надеюсь перевод данной статьи поможет вам ознакомиться с чем-то новым и полезным. Я не претендую на звание профессионального переводчика, я только начинаю переводить статьи и буду рад любым содержательным фидбекам.

То есть, в одном потоке движок JavaScript может обработать только 1 оператор за раз. JavaScript — это однопоточный язык программирования, в котором может быть выполнено только что-то одно за раз.

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

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

Используя асинхронность JavaScript(функции обратного вызова(callback’и), “промисы” и async/await) вы можете выполнять долгие сетевые запросы без блокирования основного потока. Здесь то и вступает в игру асинхронность JavaScript.

Несмотря на то, что не обязательно изучать все эти концепции, чтобы быть хорошим JavaScript-разработчиком, полезно их знать.

И так, без лишних слов, давайте начинать.

Как работает синхронный JavaScript?

Прежде чем мы углубимся в работу асинхронного JavaScript, давайте для начала разберемся как выполняется синхронный код внутри движка JavaScript. Например:

const second = () => { console.log('Hello there!');
}
const first = () => { console.log('Hi there!'); second(); console.log('The End');
}
first();

Для того, чтобы разобраться как код выше выполняется внутри движка JavaScript, нам следует понимать концепцию контекста выполнения и стека вызовов(так же известный как стек выполнения).

Контекст выполнения

Контекст выполнения — это абстрактное понятие окружения в котором код оценивается и выполняется. Всякий раз, когда какой-либо код выполняется в JavaScript он запускается в контексте выполнения.

Каждая функция имеет свой собственный контекст выполнения. Код функции выполняется внутри контекста выполнения функции, а глобальный код в свою очередь выполняется внутри глобального контекста выполнения.

Стек вызовов

Под стеком вызовов подразумевается стек со структурой LIFO(Last in, First Out/Последний вошел, первый вышел), который используется для хранения всех контекстов выполнения, созданных на протяжении исполнения кода.

Структура LIFO означает, что элементы могут добавляться и удаляться только с вершины стека. В JavaScript имеется только один стек вызовов, так как это однопоточный язык программирования.

Давайте теперь вернемся к фрагменту кода выше и попробуем понять, как движок JavaScript его выполняет.

const second = () => { console.log('Hello there!');
}
const first = () => { console.log('Hi there!'); second(); console.log('The End');
}
first();

И так, что же здесь произошло?

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

После этого мы вызываем функцию second(), поэтому она помещается на вершину стека. Далее, на вершину стека вызовов помещается console.log('Hi there!'), после выполнения он удаляется из стека.

Функция second() завершена, она также удаляется из стека. console.log('Hello there!') добавлен на вершину стека и удаляется из него по завершению выполнения.

После этого функция first() завершается и также удаляется из стека. console.log('The End') добавлен на вершину стека и удален по завершению.

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

Как работает асинхронный JavaScript?

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

Что такое блокирование?

Давайте предположим, что мы выполняем обработку изображения или сетевой запрос синхронно. Например:

const processImage = (image) => { /** * Выполняем обработку изображения **/ console.log('Image processed');
}
const networkRequest = (url) => { /** * Обращаемся к некоторому сетевому ресурсу **/ return someData;
}
const greeting = () => { console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

Обработка изображения и сетевой запрос требует времени. Когда функция processImage() вызвана её выполнение потребует некоторого времени, в зависимости от размера изображения.

После нее вызывается и добавляется в стек функция networkRequest(). Когда функция processImage() выполнена она удаляется из стека. Это снова займет некоторое время прежде чем завершить выполнение.

В конце концов, когда функция networkRequest() выполнена, вызывается функция greeting(), поскольку она содержит только метод console.log, а этот метод, как правило, выполняется быстро, функция greeting() выполнится и завершится мгновенно.

Это означает, что такие функции блокируют стек вызовов или основной поток. Как вы видите, нам нужно ждать пока функция(такие как processImage() или networkRequest()) завершится. По итогу мы не можем выполнить другие операции, пока код выше не будет выполнен.

Так какое же решение?

Самое простое решение — это асинхронные функции обратного вызова. Мы используем их, чтобы сделать наш код неблокируемым. Например:

const networkRequest = () => , 2000);
};
console.log('Hello World');
networkRequest();

Здесь я использовал метод setTimeout для того чтобы имитировать сетевой запрос. Пожалуйста, помните, что setTimeout не является частью движка JavaScript, это часть так называемого web API(в браузере) и C/C++ APIs (в node.js).

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

В Nodejs, web APIs заменяется на C/C++ APIs. Цикл обработки событий, web API и очередь сообщений/очередь задач не являются частью движка JavaScript, это часть браузерной среды выполнения JavaScript или среды выполнения JavaScript в Nodejs(в случае Nodejs).

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

const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');

Далее встречается вызов функции networkRequest(), он добавляется на вершину стека. Когда код приведенный выше загружается в браузер console.log('Hello World') добавляется в стек и удаляется из него по завершению выполнения.

Функция setTimeout() имеет 2 аргумента: 1) функция обратного вызова и 2) время в миллисекундах. Следующая вызывается функция setTimeout() и помещается на вершину стека.

На этом этапе, setTimeout() завершается и удаляется из стека. setTimeout() запускает таймер на 2 секунды в окружении web API. После этого, в стек добавляется console.log('The End'), выполняется и удаляется из него по завершению.

Но обратный вызов не может быть немедленно выполнен, и именно здесь в процесс вступает цикл обработки событий. Тем временем таймер истек, теперь обратный вызов добавляется в очередь сообщений.

Цикл обработки событий

Задача цикла обработки событий заключается в том чтобы следить за стеком вызовов и определять пуст он или нет. Если стек вызовов пустой, то цикл обработки событий заглядывает в очередь сообщений, чтобы узнать есть ли обратные вызовы, которые ожидают своего выполнения.

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

На этом моменте обратный вызов выполнен и удален из стека, а программа полностью завершена. После console.log('Async Code') добавляется на вершину стека, выполняется и удаляется из него.

События DOM

Очередь сообщений также содержит обратные вызовы от событий DOM, такие как клики и “клавиатурные” события. Например:

document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked');
});

В случае с событиями DOM, обработчик событий находится в окружении web API, ожидая определенного события(в данном случае клик), и когда это событие происходит функция обратного вызова помещается в очередь сообщений, ожидая своего выполнения.

Мы изучил как выполняются асинхронные обратные вызовы и события DOM, которые используют очередь сообщений для хранения обратных вызовов ожидающих своего выполнения.

ES6 Очередь микротасков

Прим. автора перевода: В статье автор использовал message/task queue и job/micro-taks queue, но если перевести task queue и job queue, то по идее это получается одно и то же. Я поговорил с автором перевода и решил просто опустить понятие job queue. Если у вас есть какие-то свои мысли на этот счет, то жду вас в комментариях

Разница между очередью сообщений и очередью микротасков состоит в том, что очередь микротасков имеет более высокий приоритет по сравнению с очередью сообщений, это означает, что “промисы” внутри очереди микротасков будут выполняться раньше, чем обратные вызовы в очереди сообщений. ES6 представил понятие очередь микротасков, которые используются “промисами” в JavaScript.

Например:

console.log('Script start');
setTimeout(() => { console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err));
console.log('Script End');

Вывод:

Script start
Script End
Promise resolved
setTimeout

Как вы можете видеть “промис” выполнился раньше setTimeout, все это из-за того, что ответ “промиса” хранится внутри очереди микростасков, которая имеет более высокий приоритет, нежели очередь сообщений.

Давайте разберем следующий пример, на этот раз 2 “промиса” и 2 setTimeout:

console.log('Script start');
setTimeout(() => { console.log('setTimeout 1');
}, 0);
setTimeout(() => { console.log('setTimeout 2');
}, 0);
new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err));
new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err));
console.log('Script End');

Вывод:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2

И снова оба наших “промиса” выполнились раньше обратных вызовов внутри setTimeout, так как цикл обработки событий считает задачи из очереди микротасков важнее, чем задачи из очереди сообщений/очереди задач.

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

Например:

console.log('Script start');
setTimeout(() => { console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res));
new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res));
console.log('Script End');

Вывод:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout

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

Заключение

И так, мы изучил как работает асинхронный JavaScript и другие понятия, такие как стек вызовов, цикл обработки событий, очередь сообщений/очередь задач и очередь микротасков, которые вместе представляют собой среду выполнения JavaScript.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»