Полное понимание обещаний, ожидания и асинхронности 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 функции всегда немедленно возвращают обещание.

Удачи!