Обзор JavaScript Promises

Как стать разработчиком JavaScript Изучение

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

Что такое JavaScript Promises?

В JavaScript некоторые операции асинхронны. Это означает, что результат или значение, которые они производят, не доступны сразу после завершения операции.

Promises — это специальный объект JavaScript, который представляет конечный результат такой асинхронной операции. Он действует как прокси для результата операции.

Старые недобрые времена: функции обратного вызова

До того, как у нас появились обещания JavaScript, предпочтительным способом работы с асинхронной операцией было использование обратного вызова. Обратный вызов — это функция, которая запускается, когда готов результат асинхронной операции. Например:

setTimeout(function() {
  console.log('Hello, World!');
}, 1000);

Вот setTimeoutасинхронная функция, которая запускает любую функцию обратного вызова, переданную через указанное количество миллисекунд. В этом случае он регистрирует «Hello, World!» на консоль по прошествии одной секунды.

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

setTimeout(function() {
  console.log(1);
  setTimeout(function() {
    console.log(2);
    setTimeout(function() {
      console.log(3);
      setTimeout(function() {
        console.log(4);
        setTimeout(function() {
          console.log(5);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

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

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

Читайте также:  Всесторонний обзор основ компьютерных сетей

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

Как создать объект обещания JavaScript

Основной синтаксис для создания промиса следующий:

const promise = new Promise((resolve, reject) => {
  //asynchronous code goes here
});

Мы начинаем с создания экземпляра нового объекта обещания с помощью Promiseконструктора и передачи ему функции обратного вызова. Обратный вызов принимает два аргумента resolveи reject, которые являются функциями. Весь наш асинхронный код находится внутри этого обратного вызова.

Если все работает успешно, обещание выполняется вызовом resolve. В случае ошибки обещание отклоняется вызовом reject. Мы можем передавать значения обоим методам, которые затем будут доступны в потребляющем коде.

Чтобы увидеть, как это работает на практике, рассмотрим следующий код. Это делает асинхронный запрос к веб-сервису, который возвращает случайную шутку папы в формате JSON:

const promise = new Promise((resolve, reject) => {
  const request = new XMLHttpRequest();
  request.open('GET', 'https://icanhazdadjoke.com/');
  request.setRequestHeader('Accept', 'application/json');

  request.onload = () => {
    if (request.status === 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = () => {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

  request.send(); // send the request
});

Конструктор обещаний

Мы начинаем с использования Promiseконструктора для создания нового объекта обещания. Конструктор используется для переноса функций или API-интерфейсов, которые еще не поддерживают промисы, например XMLHttpRequestобъект выше. Обратный вызов, переданный конструктору обещаний, содержит асинхронный код, используемый для получения данных из удаленной службы. (Обратите внимание, что здесь мы используем функцию стрелки.) Внутри обратного вызова мы создаем запрос Ajax к https://icanhazdadjoke.com/, который возвращает случайную шутку папы в формате JSON.

Когда от удаленного сервера получен успешный ответ, он передается resolveметоду. В случае возникновения какой-либо ошибки — либо на сервере, либо на сетевом уровне — rejectвызывается с Errorобъектом.

Метод then

Когда мы создаем объект обещания, мы получаем прокси для данных, которые будут доступны в будущем. В нашем случае мы ожидаем, что некоторые данные будут возвращены удаленной службой. Итак, как мы узнаем, когда данные станут доступны? Здесь используется Promise.then()функция:

const promise = new Promise((resolve, reject) => { ... });

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  document.body.textContent = JSON.parse(data).joke;
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

Эта функция может принимать два аргумента: обратный вызов при успешном выполнении и обратный вызов при отказе. Эти обратные вызовы вызываются, когда обещание выполнено (то есть либо выполнено, либо отклонено). Если обещание было выполнено, обратный вызов успеха будет запущен с фактическими данными, которые мы передали resolve. Если обещание было отклонено, будет вызван обратный вызов отказа. Все, что мы передали, rejectбудет передано в качестве аргумента этому обратному вызову.

Каковы состояния JavaScript promise?

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

Промис может находиться в одном из следующих состояний:

  • pending
  • fulfilled
  • rejected
  • settled

Обещание начинает жизнь в состоянии ожидания. Это означает, что оно не было ни выполнено, ни отвергнуто. Если действие, связанное с обещанием, выполнено успешно (в нашем случае это удаленный вызов API) и resolveвызывается метод, считается, что обещание выполнено. Если, с другой стороны, связанное действие оказывается неудачным и rejectметод вызывается, промис находится в состоянии отклонения. Наконец, считается, что обещание выполнено, если оно либо выполнено, либо отклонено, но не находится в ожидании.

Обещание начинает жизнь в состоянии ожидания

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

Но разве мы не должны использовать Fetch API?

В этот момент мы можем спросить, почему мы не используем Fetch API для получения данных с удаленного сервера, и ответ заключается в том, что, вероятно, нам следует это делать.

В отличие от XMLHttpRequestобъекта, Fetch API основан на промисах, что означает, что мы можем переписать наш код следующим образом (минус обработка ошибок):

fetch('https://icanhazdadjoke.com', { 
  headers: { 'Accept': 'application/json' }
})
  .then(res => res.json())
  .then(json => console.log(json.joke));

Причина использования XMLHttpRequestзаключалась в том, чтобы дать больше информации о том, что происходит под капотом.

Цепочка Promises

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

Мы могли бы начать с создания нового объекта обещания, как мы это делали ранее:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
});

Как и ожидалось, промис разрешается по прошествии секунды и в консоль записывается «1».

Чтобы продолжить цепочку, нам нужно вернуть второе обещание после нашего оператора консоли и передать его второму then:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => { resolve() }, 1000)
});

promise.then(() => {
  console.log(1);
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve() }, 1000)
  });
}).then(() => {
  console.log(2);
});

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

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Затем мы можем использовать это, чтобы сгладить наш вложенный код:

sleep(1000)
  .then(() => {
    console.log(1);
    return sleep(1000);
  }).then(() => {
    console.log(2);
    return sleep(1000);
  }).then(() => {
    console.log(3);
    return sleep(1000);
  })
  ...

И поскольку сам thenметод возвращает объект обещания, и мы не передаем никаких значений от одной асинхронной операции к другой, это позволяет нам еще больше упростить ситуацию:

sleep(1000)
  .then(() => console.log(1))
  .then(() => sleep(1000))
  .then(() => console.log(2))
  .then(() => sleep(1000))
  .then(() => console.log(3))
  ...

Передача данных по цепочке обещаний

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

Например, нам может понадобиться получить список участников репозитория GitHub, а затем использовать эту информацию для получения имени первого участника:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ESLint was ${json.name}`));

// The first contributor to ESLint was Nicholas C. Zakas

Как мы видим, возвращая обещание, возвращенное из второго вызова fetch, ответ сервера ( res) доступен в следующем thenблоке.

Promise обработки ошибок

Мы уже видели, что thenфункция принимает две функции обратного вызова в качестве аргументов и что вторая будет вызвана, если обещание было отклонено:

promise.then((data) => {
  console.log('Got data! Promise fulfilled.');
  ...
}, (error) => {
  console.error('Promise rejected.');
  console.error(error.message);
});

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

Метод catch

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

В качестве примера возьмем предыдущий код:

fetch('https://api.github.com/repos/eslint/eslint/contributors')
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.jsn())
  .then(json => console.log(`The top contributor to ESLint wass ${json.name}`))
  .catch(error => console.log(error));

Обратите внимание, что в дополнение к добавлению обработчика ошибок в конце блока кода я допустил ошибку res.json()as res.jsnв седьмой строке.

Теперь, когда мы запускаем код, мы видим следующий вывод на экран:

TypeError: res.jsn is not a function
  <anonymous>  http://0.0.0.0:8000/index.js:7  
  promise callback*  http://0.0.0.0:8000/index.js:7  

index.js:9:27

Файл, в котором я работаю, называется index.js. Строка 7 содержит ошибку, а строка 9 — это catchблок, который ее поймал.

Метод finally

Метод Promise.finallyзапускается, когда обещание выполнено, то есть либо разрешено, либо отклонено. Подобно catch, он помогает предотвратить дублирование кода и весьма полезен для выполнения задач очистки, таких как закрытие соединения с базой данных или удаление счетчика загрузки из пользовательского интерфейса.

Вот пример использования нашего предыдущего кода:

function getFirstContributor(org, repo) {
  showLoadingSpinner();
  fetch(`https://api.github.com/repos/${org}/${repo}/contributors`)
  .then(res => res.json())
  .then(json => {
    const firstContributor = json[0].login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
  })
  .then(res => res.json())
  .then(json => console.log(`The first contributor to ${repo} was ${json.name}`))
  .catch(error => console.log(error))
  .finally(() => hideLoadingSpinner());
};

getFirstContributor('facebook', 'react');

Он не получает никаких аргументов и возвращает обещание, так что мы можем связать больше then, catch, и finallyвызвать его возвращаемое значение.

Дальнейшие методы Promise

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

Promise.all()

В отличие от предыдущего примера, где нам нужно было завершить первый вызов Ajax, прежде чем мы сможем сделать второй, иногда у нас будет куча асинхронных операций, которые вообще не зависят друг от друга. Это когда Promise.allвходит.

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

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

Приведенный выше код войдет [1, 2, 3]в консоль через три секунды.

Однако, если какое-либо из обещаний будет отклонено, allбудет отклонено значение этого обещания и не будут приниматься во внимание никакие другие обещания.

Promise.allSettled()

В отличие от all, Promise.allSettledбудет ждать выполнения или отклонения каждого переданного промиса. Он не остановит выполнение в случае отклонения обещания:

Promise.allSettled([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => reject(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

Это вернет список статусов и значений (если обещание выполнено) или причин (если оно было отклонено):

[
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: 2 },
  { status: "fulfilled", value: 3 },
]

Promise.any()

Promise.anyвозвращает значение первого обещания, которое должно быть выполнено. Если какие-либо обещания отклонены, они игнорируются:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(1), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

Это записывает «2» в консоль через полторы секунды.

Promise.race()

Promise.raceтакже получает массив обещаний и (как и другие перечисленные выше методы) возвращает новое обещание. Как только одно из обещаний, которые он получает, выполняется или отвергается, оно raceсамо либо выполняет, либо отвергает со значением или причиной из только что установленного обещания:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject('Rejected with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

Это запишет «Отклонено с 1» в консоль, так как первое промис в массиве немедленно отклоняется, и отклонение перехватывается нашим catchблоком.

Мы могли бы изменить такие вещи:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve('Resolved with 1'), 0)),
  new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
  .then(values => console.log(values))
  .catch(err => console.error(err));

Это зарегистрирует «Решено с 1» на консоли.

В обоих случаях два других обещания игнорируются.

Заключение

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

Как упоминалось выше, отличным следующим шагом было бы начать изучать async. Awaitи углублять свое понимание управления потоком внутри программы JavaScript.

Оцените статью
bestprogrammer.ru
Добавить комментарий