И немного под капотом.

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

Обратные вызовы

Если мы должны быть на 100% правы в том, что такое обратный вызов, то обратный вызов — это функция, которую мы передаем в другую функцию, которая принимает другую функцию в качестве аргумента. И эта функция, которую мы передаем, может быть вызвана в любое время в будущем функцией, в которую мы ее передаем. Затем он называется higher order function , который принимает функцию в качестве аргумента.

Это обратный вызов:

function someFunctionAcceptingCallback(number, callback){
return callback(number + 10)
}
function divide(answer) {
return answer / 2
}
someFunctionAcceptingCallback(5, divide) // 7.5 if we console.log it

Еще один хороший пример — addEventListener в javascript.

document.getElementById('addUser').addEventListener('click', function() {// Do something})

Итак, что здесь происходит, так это то, что addEventListener позволяет нам подождать с выполнением до определенного момента позже, в этом случае, когда нажимается кнопка addUser, только тогда наша функция callback выполняется. Это асинхронное событие.

Но как это вообще работает?.. Javascript является однопоточным, что означает, что он не может выполнять несколько кодов одновременно, у javascript есть call stack, который запускает 1 задачу за раз сверху вниз.

Поэтому, когда мы добавляем наш addEventListener , мы на самом деле не вызываем нативный метод javascript, мы вызываем метод в WEB API. И WEB API мы можем визуализировать в основном как еще один поток.

Таким образом, WEB API addEventListener является событием DOM, он ожидает нажатия нашей кнопки, затем, когда он нажимается, он принимает нашу функцию callback, которую мы передали в качестве аргумента, и передает ее чему-то, называемому task queue/callback queue, а затем чему-то, называемому event loop выбирает функцию обратного вызова и помещает ее в call stack, и она выполняется, но только тогда, когда функция обратного вызова является первой функцией в task queue/callback queue . Я сделал наглядный пример того, как это работает:

Какой прекрасный пример хорошего использования callback-функции?.. Вы правильно догадались, сетевые запросы.

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

Но поскольку javascript является однопоточным, не было возможности сделать это изначально в javascript до того, как обещания были введены в ec6.

Что произошло бы, если бы мы вызывали сетевой запрос без использования какого-либо WEB API, так это то, что наш код был бы заблокирован, call stack не выполнял бы никакого кода до того, как наши сетевые запросы были бы выполнены. Это называется блокировкой кода, это происходит, если наш код синхронен и выполняется долго, тогда наша веб-страница была бы заморожена, код javascript не выполнялся бы, так как мы вызываем сетевой запрос прямо в стеке вызовов, и он блокирует остальную часть кода javascript для выполнения.

Таким образом, мы решили эту проблему, используя другой WEB API под названием XMLHttpRequest .

XMLHttpRequest — это WEB API, мы передаем нашу функцию обратного вызова, как только XMLHttpRequest выполняется, он отправляет ее в task queue/callback queue, а event loop подхватывает ее и отправляет в call stack для выполнения нашей функции обратного вызова.

Вот пример:

Все хорошо, верно? Мы можем сделать сетевые запросы асинхронными с помощью WEB API и передать функции обратного вызова, которые он должен выполнять после получения ответа от сетевого запроса.

Подвох

ад обратного звонка

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

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

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

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

Обещания

Что такое обещание тогда?

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

Когда были введены промисы, это было огромной проблемой для разработчиков javascript, теперь вы могли легко писать асинхронные сетевые запросы и избежать ада обратных вызовов.

Прежде чем мы говорили о том, что XMLHttpRequest был веб-API, потому что javascript является однопоточным, и мы должны передавать наши асинхронные функции чему-то другому, кроме call stack, чтобы избежать блокировки кода.

Но промисы не используют никаких callback queue , поэтому, если это так, что промисы используют для асинхронности?

Микро очередь

Это было введено с ec6, в основном это похоже на micro queue, но для промисов и немного отличается. micro queue имеет приоритет над callback queue , поэтому, например, если мы запустим этот код:

Затем, несмотря на то, что наш setTimeout запустит обратный вызов сразу через 0 секунд, а промис также настроен на это, что является немедленным, промис запустит свой обратный вызов первым, потому что он использует micro queue и имеет приоритет над callback queue, который setTimout Используется метод WEB API.

Давайте покопаемся в Promise, у promises есть 3 состояния, pending fulfilled rejected.

pending означает, что обещание не было разрешено или отклонено в контексте запроса API, когда мы делаем запрос, а сервер не отправляет никакого ответа, тогда обещание находится в состоянии ожидания.

fulfilled означает, что обещание разрешено в контексте запроса API, когда мы получим успешный ответ, тогда обещание будет разрешено.

rejected означает, что наше обещание было отклонено в контексте запроса API, когда мы получаем ответ 404, именно тогда обещание будет отклонено, и мы можем catch ошибиться.

Мы можем, например, создать новое обещание с тайм-аутом в 3 секунды, и по прошествии 3 секунд мы можем выполнить обещание. Это означает, что мы можем делать что-то после того, как что-то было завершено на чистом нативном JavaScript, без использования библиотеки для асинхронного выполнения. И это касается любого выполнения, на которое мы не знаем, когда получим ответ.

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

Мы также можем обернуть fetch в обещание:

Fetch также основан на промисе, поэтому, когда мы вызываем .then() на fetch , на самом деле мы ждем, пока сервер не разрешит промис/не вернет ответ.

Цепочка

Вместо ада обратного вызова мы можем связать промисы разными способами, например, если мы хотим сделать еще один запрос после того, как наш первый запрос был разрешен, мы можем сделать это:

Здесь я получил задачу, затем мы превратили ответ в JSON, затем мы разрешили его, затем мы делаем .then()на myPromise, что будет разрешенным значением обещания. Затем мы вызываем функцию doSomething() с нашими данными JSON, а затем регистрируем, что все готово.

Обещание.все

Промисы имеют что-то под названием Promise.all, что позволяет нам дождаться разрешения любого количества промисов, а затем выполнить блок кода. Это может быть очень-очень полезно, например, если у нас будет массив запросов, которые нам нужно вызвать, и дождаться завершения всех запросов, тогда мы можем сделать это в нескольких строках кода, например:

Асинхронный ждет

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

Вот что такое асинхронное ожидание.

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

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

Итак, с цепочкой промисов мы делаем следующее:

Когда оптимальный способ написания синхронизированного асинхронного кода будет выглядеть так:

Это идея async-await.

Итак, правильный способ написать async-await таков:

Вместо того, чтобы делать fetchData().then(resolvedData)..., нам просто нужно сделать const data = await fetchData(), и data будет содержать разрешенные данные, а следующая строка не будет выполняться, пока fetchData() не разрешит свое обещание.

Следует помнить, что когда у нас есть асинхронная функция, это в основном то же самое, что обещание, на самом деле вся функция является обещанием. И мы делаем то же самое, что и когда обещание разрешается для асинхронной функции. Итак, что мы можем сделать, так это asyncFunction.then() выполнить код после того, как функция разрешила свое обещание, что происходит после того, как все наши awaits разрешены, и мы возвращаемся из функции.

Итак, что произойдет в нашем asyncFunc(), так это то, что сначала он сделает запрос fetchData() и будет ждать, пока обещание не будет разрешено сервером. Затем, когда он разрешается, он вызывает запрос fetchUserData() и ждет, пока обещание не будет разрешено сервером. Тогда, только тогда он обновит userState .

И если мы хотим вызвать эту функцию и дождаться, пока она выполнит все эти шаги, мы просто делаем asyncFunc().then() , то же самое мы сделали для нашего промиса, потому что асинхронность — это промис, и когда мы возвращаем что-то из функции, это в основном так же, как мы разрешаем обещание.

Заключение

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

Возможно, вам также понравятся:







Привет, меня зовут Антон 👋, я внештатный фронтенд-разработчик из Швеции.

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