Полное понимание обещаний, ожидания и асинхронности Javascript
Это эссе для новичков и для всех, кто время от времени не понимает, что делает javascript wtf, когда дело касается асинхронного потока.
В этом примере мы собираемся загрузить мои пользовательские данные из api github. Мы собираемся представить, что это находится в приложении, которое выполняет множество других вещей. Возможно, это веб-сервер, и пока он ждет github, он может с удовольствием обслуживать другие веб-запросы.
Итак, вот стандартный, олдскульный способ сделать это. Обратный звонок!
const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; function getUserDataWithCallback(cb) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState == 4) { cb(xhr.responseText) } } xhr.open('get', 'https://api.github.com/users/bluejack', true) xhr.send() } getUserDataWithCallback(function(text) { console.log(text) })
Большой. Это работает. Чисто, ясно, очевидно.
Но чтобы убедиться, что все со мной, вот что происходит:
(1) поток возвращает объект XMLHttpRequest
из оператора require.
(2) поток затем вызывает getUserDataWithCallback
, который передает анонимную функцию в качестве первого параметра этой функции.
(3) поток создает объект для обработки этого конкретного запроса: xhr
- и мы немедленно регистрируем другую функцию для этого объекта с помощью xhr.onreadystatechange
: нашего обработчика, что делать, когда запрос завершен.
(4) поток открывает соединение и отправляет запрос.
(5) поток возвращается к вызывающей функции и полностью покидает скрипт.
(6) Я запускаю это в узле. Процесс не завершается, потому что мы все еще удерживаем сетевой ресурс.
(7) В конце концов сетевой ресурс возвращается, и состояние объекта xhr
становится таинственным магическим 4, и XMLHttpRequest
вызывает функцию, которую мы зарегистрировали для этого события. Теперь у нас есть текст, который мы можем использовать.
(8) поток вызывает функцию обратного вызова с этим текстом
(9) функция обратного вызова записывает текст в консоль.
Я подробно рассмотрел этот поток, потому что, судя по тому, что я видел при переполнении стека, многие новички не полностью понимают поток даже обратных вызовов, и если вы этого не поймете, вы, конечно, не поймете, что следует за ним.
С этого момента мы будем держать все в узде.
Важно понимать, что после того, как поток выйдет из конца сценария, единственный поток выполнения будет доступен для выполнения другой работы, будь это в более крупном приложении. Веб-сервер, обслуживающий страницы. Только когда сетевой запрос будет выполнен и состояние изменится, наш код «вернется к жизни».
Итак, обратный вызов - это простой способ обработки асинхронных событий. Это кажется довольно чистым ... пока вы не начнете вникать в сорняки асинхронных событий, основанных на других асинхронных событиях. Внезапно мы начинаем попадать в то, что многие называют адом обратных вызовов - десятки вложенных обратных вызовов с различными ветвями для обработки случаев ошибок где-то в процессе. "Это ужасно".
Следующим инструментом в наборе инструментов является Обещание.
Обещания, обещания.
Обещания обещают сделать жизнь лучше.
Вот стандартная реализация на основе обещаний:
const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; function getUserDataWithPromise() { var xhr = new XMLHttpRequest(); return new Promise(function(resolve, reject) { xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status >= 300) { reject("Error, status code = " + xhr.status) } else { resolve(xhr.responseText); } } } xhr.open('get', 'https://api.github.com/users/bluejack', true) xhr.send(); }); } getUserDataWithPromise().then(function(result) { console.log(result) }, function(error) { console.log(error) })
Если вы посмотрите на это, на самом деле обещание - это просто причудливый способ упаковать обратный вызов.
Процесс в точности такой же, как и с обратным вызовом, но вместо того, чтобы ничего не возвращать (в случае обратного вызова), он возвращает объект Promise, который был создан с помощью одной функции ... тела нашей работы .
Promise выполняет эту функцию и передает ей две собственные функции: resolve
и reject
. Когда наша функция вызывает разрешение или отклонение, это имеет тот же эффект, что и обратный вызов, как вы вскоре увидите.
В этом примере используется стандартный короткий синтаксис. getUserDataWithPromise
возвращает объект Promise, и мы немедленно вызываем then
для этого объекта и передаем ему две функции. Первая вызывается, когда наш рабочий код вызывает resolve
, а вторая функция вызывается, когда наш рабочий код вызывает reject
.
Хотя это может показаться более запутанным, чем простой обратный вызов, его слава заключается в том, что это стандартный способ обработки случаев ошибок, и Promises можно легко и чисто связать с консолидированной обработкой ошибок. Обратного ада больше нет.
Чтобы сделать это немного более явным, вы также можете написать наш внешний код следующим образом:
var userDataPromise = getUserDataWithPromise() userDataPromise.then(function(result) { console.log(result) }, function(error) { console.log(error) })
Все ясно, правда?
Но оказывается, что для некоторых обещаний недостаточно, поэтому у нас также есть await
и async
. Проблема с обещаниями заключается в том, что они могут быть громоздкими и тяжелыми и немного сбивать с толку, когда закончилось управление потоком, когда все, что мы хотим сделать, - это дождаться чего-то, что произойдет за пределами нашего единственного процесса Javascript. Обещает очистить обработку ошибок… но только до определенной степени.
Давай await
больше не будем.
Await, Async и Syntactic Sugar Oh My!
Поскольку вы понимаете промисы, давайте начнем с async
- это ключевое слово, используемое для объявления функции. Он гарантирует, что эта функция вернет обещание, и указывает, что эта функция может иметь внутри асинхронное поведение. Более того, вам не нужно писать код обещания!
async function getUserData() { return "Bluejack" }
Эта функция, вопреки внешнему виду, не возвращает строку «Bluejack» - она возвращает обещание, которое преобразуется в строку «Bluejack»:
Смотреть:
$vim blue.js: async function getUserData() { return "Bluejack" } console.log(getUserData()) $ node blue.js: Promise { 'Bluejack' }
Вместо этого отредактируйте наш вызов, чтобы правильно использовать обещание:
getUserData().then(function (val) { console.log(val) }) $ node blue.js Bluejack
Я предпочитаю, чтобы мой код был до боли ясным, поэтому мне не нравится, когда Javascript дает обещания от моего имени. Но есть еще одна причина для ключевого слова async
, и это то, что мы можем использовать ключевое слово await
. Когда мы используем await
, это должно быть в объявленной функции async
.
await
является альтернативой then
в работе с обещаниями, и в результате получается код, который выглядит синхронным, но не блокирует наш процесс. Поскольку его можно использовать только в функции, помеченной async
, его нельзя использовать на верхнем уровне кода.
Вот обновление нашего примера.
const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; async function getUserDataWithPromise() { var xhr = new XMLHttpRequest(); return new Promise(function(resolve, reject) { xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status >= 300) { reject("Error, status code = " + xhr.status) } else { resolve(xhr.responseText); } } } xhr.open('get', 'https://api.github.com/users/bluejack', true) xhr.send(); }); } async function logUserData() { try { let user = await getUserDataWithPromise() console.log(user) } catch (err) { console.log(err) } } logUserData()
Итак: нам все еще нужен getUserDataWithPromise()
, потому что XMLHttpRequest.onreadystatechange
- это «пробуждающий сигнал», который запускает весь нисходящий поток, и мы не можем ждать этого с помощью синтаксической асинхронности и ожидания. В наши дни большинство функций и библиотек сетевых функций будут использовать обещания, так что это было бы прекрасно, используя fetch
или axios
.
Но теперь мы можем инкапсулировать этот устаревший код с помощью чистого современного javascript. Используя await
, мы пишем асинхронную функцию, которая выглядит синхронной: logUserData()
.
Одно из достоинств async
/ await
заключается в том, что код может выглядеть очень чистым. Кроме того, использование блоков try/catch
для обработки reject
функции одного или нескольких вложенных отклоненных обещаний может быть как элегантным, так и гораздо более стабильным (если вы не забываете их использовать).
Но здесь есть несколько ошибок, которые, кажется, регулярно сбивают с толку разработчиков.
Во-первых, если обещания не будут хорошо поняты, await/async
может заманить нас в ложное ощущение синхронного поведения. Помните, что в нашем примере logUserData()
немедленно возвращается с обещанием просто потому, что оно объявлено async
. В данном случае это обещание, которое не предоставляет нам никаких данных, поскольку мы ничего не возвращаем. Мы могли ошибиться, думая, что вызываем logUserData()
и получаем результат только после того, как наш await
завершится. Нет нет. Не так.
Во-вторых, анонимное создание обещаний в результате ключевого слова async
- это небольшая забавная магия, которая, кажется, вызывает бесконечную путаницу. Помните, что обещание преобразуется в любой результат, возвращаемый функцией, включая undefined
, если у вас нет явного возврата.
Истинный ключ к пониманию async
и await
заключается в том, что они являются синтаксическими особенностями языка javascript, предназначенными для обработки обещаний в рамках try/catch
модели обработки ошибок, но низкоуровневое понимание того, что обещание - это просто причудливый способ инкапсуляции обратного вызова, и понимание высокого уровня того, что async
функции всегда немедленно возвращают обещание.
Удачи!