Что такое Promise в JavaScript

2 года назад·6 мин. на чтение

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

Современная веб-разработка в значительной степени опирается на асинхронное программирование, что позволяет нам эффективно обрабатывать трудоемкие операции, не откладывая выполнение других задач. Цель этой статьи — дать полное представление о промисах JavaScript, включая информацию об их определении, этапах жизненного цикла, вспомогательных функциях и внутренней работе.

Что такое Promises в JavaScript?

Успех (или сбой) асинхронной операции и значение, которое она генерирует, представлены объектом, называемым обещанием. Вместо вложения обратных вызовов он позволяет нам обрабатывать асинхронный код более элегантным и организованным способом с помощью цепочек методов. Обещания могут находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнен) или rejected (отклонен). Pending - начальное состояние Promise, которое означает, что асинхронная операция все еще выполняется. Если операция выполнена успешно, Promise переходит в состояние “выполнен”, а при возникновении проблемы — в состояние “отклонен”.

Этапы жизненного цикла Promise

Давайте подробно рассмотрим каждый этап жизненного цикла Promise на примерах кода:

Ожидание

Объект Promise находится в состоянии ожидания на момент создания. На данный момент асинхронная операция все еще продолжается, и обещание не принимается и не отклоняется. Вот пример:
const promise = new Promise((resolve, reject) => {
  // Асинхронная операция, например, запрос данных через API,
  // resolve(result) или reject(error) будут вызваны позднее
});

Выполнено (Fulfilled)

Обещание переходит в состояние “выполнено”, как только асинхронная операция успешно завершена. На этом этапе связанное значение (результат) становится доступным. Для обработки выполненного обещания мы используем метод .then() Вот пример:
const promise = new Promise((resolve, reject) => {
  // Симуляция асинхронной операции
  setTimeout(() => {
    resolve("Operation succeeded!");
  }, 2000);
});

promise.then((result) => {
  console.log(result); // Вывод: "Operation succeeded!"
});

Отклонено (Rejected)

В случае, если возникает проблема с асинхронной операцией, Promise переходит в отклоненное состояние. Оно обозначает, что операция не удалась, и предоставляет объекту ошибки соответствующую информацию. Для обработки отклоненного Promise мы используем метод .catch() Вот пример:
const promise = new Promise((resolve, reject) => {
  // Симуляция асинхронной операции
  setTimeout(() => {
    reject(new Error("Something went wrong!"));
  }, 2000);
});

promise.catch((error) => {
  console.log(error.message); // Вывод: "Something went wrong!"
});

Цепочка обещаний

Обещания имеют ряд важных преимуществ, в том числе возможность объединять несколько асинхронных операций, что улучшает читаемость кода. Мы достигаем этого, используя метод .then() для возврата нового Promise. Вот пример:
const getUser = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронной операции
    setTimeout(() => {
      resolve({ id: 1, name: "John" });
    }, 2000);
  });
};

const getUserPosts = (user) => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронной операции
    setTimeout(() => {
      resolve(["Post 1", "Post 2"]);
    }, 2000);
 });
};

getUser()
  .then((user) => getUserPosts(user))
  .then((posts) => console.log(posts)); // Вывод: ["Post 1", "Post 2"]

Вспомогательные функции для объектов Promise

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

Promise.all()

Когда все объекты Promise во входном массиве выполнены, эта функция возвращает новый объект Promise. Вот пример:
const fetchUser = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const user = { id: 1, name: "John" };
      resolve(user);
    }, 2000);
  });
};

const fetchPosts = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const posts = ["Post 1", "Post 2"];
      resolve(posts);
    }, 1500);
  });
};

const fetchComments = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const comments = ["Comment 1", "Comment 2"];
      resolve(comments);
    }, 1000);
  });
};

Promise.all([fetchUser(), fetchPosts(), fetchComments()])
  .then(([user, posts, comments]) => {
    console.log("User:", user);
    console.log("Posts:", posts);
    console.log("Comments:", comments);
  })
  .catch((error) => {
    console.log("Error:", error);
  });
Три функции fetchUser(), fetchPosts() и fetchComments() включены в приведенный выше пример. Для пользовательских данных, пользовательских сообщений и комментариев пользователей каждая функция имитирует асинхронный вызов API, возвращая Promise. Передавая массив Promise ([fetchUser(), fetchPosts(), fetchComments()]) в Promise.all(), мы создаем новый Promise, который выполняется после успешного выполнения каждого Promise в массиве. При обработке выполнения метод .then() применяет синтаксис, деструктурирующий массив, для получения разрешенных значений каждого объекта Promise. Когда в этой ситуации успешно выполняются все обещания, деструктурирование массива присваивает значения fetchUser(), fetchPosts() и fetchComments() переменным user, posts и comments соответственно. Пользователь, публикации и комментарии выводятся в консоль. Если какой-либо из Promises не удался вызывается .catch() и ошибка выводится в консоль. Promise.all() позволяет нам эффективно извлекать несколько асинхронных ресурсов и обрабатывать их все одновременно после успешного завершения каждого запроса.

Promise.race()

Как только какой-либо из объектов Promise во входном массиве выполнится, эта функция возвращает новый объект Promise, который либо выполняется, либо отклоняется. Вот пример:
const fetchResource = (resource, delay) => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      resolve(`${resource} is fetched successfully in ${delay}ms`);
    }, delay);
  });
};

const resource1 = fetchResource("Resource 1", 2000);
const resource2 = fetchResource("Resource 2", 1500);
const resource3 = fetchResource("Resource 3", 1000);

Promise.race([resource1, resource2, resource3])
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });
В приведенном выше примере есть три функции, называемые fetchResource() которые имитируют асинхронные вызовы API, возвращая Promises. Каждый вызов имитирует время, необходимое для получения определенного ресурса, с разным временем задержки. Когда массив Promises ([resource1, resource2, resource3]) передается методу Promise.race(), создается новый Promise, который выполняется (выполняется успешно или отклоняется) в ответ на любое Promise в массиве Promise.race(). Значение успешного Promise передается в качестве параметра result и выводится в консоли в методе .then(), который используется для обработки выполнения. В этом случае победителем будет считаться тот ресурс, который разрешится первым (т.е. тот, у которого наименьшая задержка), а его значение будет выведено на консоль. Если какой-либо из Promises не удался, то вызывается .catch() и ошибка выводится в консоли. Мы можем выполнить несколько асинхронных операций одновременно и отреагировать на результат самой быстрой с помощью метода Promise.race(). Это полезно в ситуациях, когда мы хотим действовать в соответствии с первоначальным ответом или завершением.

Promise.resolve() и Promise.reject()

Без необходимости дополнительных асинхронных операций эти функции позволяют создавать Promise, которые уже были выполнены или отклонены соответственно. Благодаря мощным возможностям, которые эти вспомогательные функции предлагают для манипулирования и управления Promises, асинхронное программирование JavaScript теперь более адаптируемо и выразительно. Promise.resolve() и Promise.reject() используются в следующем примере кода:
const fetchData = (shouldSucceed) => {
  if (shouldSucceed) {
    return Promise.resolve("Data fetched successfully");
  } else {
    return Promise.reject(new Error("Failed to fetch data"));
  }
};

fetchData(true)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });

fetchData(false)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });
Функция fetchData() в приведенном выше примере имеет параметр shouldSucceed, который указывает, должна ли выборка данных быть успешной или неудачной. Promise.resolve() используется для создания и возврата объекта Promise, который немедленно выполняется с сообщением «Data fetched successfully», если shouldSucceed имеет значение true. Promise.reject() используется для создания и возврата объекта Promise, который немедленно отклоняется с новым объектом Error и сообщением «Failed to fetch data», если shouldSucceed имеет значение false. Возвращенное обещание выполняется в первом вызове fetchData() с shouldSucceed, равным true, и выполнение управляется методом .then(). В result передается значение «Data fetched successfully», которое затем выводится в консоль. Во втором вызове fetchData() с shouldSucceed, равным false, возвращенный объект Promise отклоняется, и для обработки отклонения используется .catch(). Объект ошибки, содержащий сообщение «Failed to fetch data», передается в качестве параметра error и выводится в консоль. Используя Promise.resolve() и Promise.reject() мы можем легко создавать Promise, которые уже разрешены или отклонены, без необходимости дополнительных асинхронных операций. Это полезно при обработке синхронных значений или ошибок в виде объектов Promise.

Итоги

Асинхронное программирование JavaScript произвело революцию благодаря обещаниям, которые предлагают хорошо организованный и красивый способ решения трудоемких задач. Обещания помогают нам создать более удобочитаемый и поддерживаемый код. Определение объектов Promise, этапы их жизненного цикла и вспомогательные функции, таких как Promise.all(), Promise.race(), Promise.resolve() и Promise.reject() были рассмотрены в этом обширном руководстве. Мы можем эффективно обрабатывать асинхронные операции и изящно обрабатывать сценарии успеха и ошибок, понимая этапы жизненного цикла - pending, fulfilled и rejected. Разработчики могут создавать надежные и эффективные приложения, которые легко справляются со сложными асинхронными задачами, используя Promises и их вспомогательные функции. Поток управления оптимизирован, а удобочитаемость кода улучшена за счет цепочек Promise. Кроме того, вспомогательные функции JavaScript улучшают обработку ошибок и упрощают выполнение многочисленных асинхронных операций. Современная веб-разработка требует использования асинхронного программирования, а знание обещаний позволяет разработчикам создавать код, который является более чистым и простым в обслуживании. Вы можете создавать надежные приложения, которые эффективно обрабатывают асинхронные операции, интегрируя Promises в свои JavaScript проекты и используя их вспомогательные функции.

Полное руководство по асинхронному JavaScript

2 года назад·7 мин. на чтение

На пути к тому, чтобы стать разработчиком JavaScript, вы, вероятно, столкнетесь с функциями обратного вызова, промисами (promise) и async/await.

JavaScript по своей сути является синхронным или однопоточным языком. Это означает, что каждое действие выполняется одно за другим, и каждое действие зависит от выполнения предыдущего. Думайте об этом как о машинах, ожидающих на светофоре. Каждая машина должна ждать, пока заведется предыдущая.
const firstCar = 'first car';
const secondCar = 'second car';

console.log('Start ' + firstCar);
console.log('Start ' + secondCar);
Результат выполнения кода.
Start first car
Start second car
Но что произойдет, если первая машина сломается? Должны ли все остальные машины ждать? У кого есть на это время?
const firstCar = 'broken';
const secondCar = 'second car';

if (firstCar === "broken") {
  throw Error("The first car is broken. Everybody stop.");
}

console.log('Start ' + firstCar);
console.log('Start ' + secondCar);
Результат выполнения кода.
Error: The first car is broken. Everybody stop.
Не лучше ли было бы, чтобы каждая машина не зависела от предыдущей? Почему нас должно волновать, если какая-то машина сломана? Если моя машина работает, почему я должен ждать, пока кто-нибудь заведет ее машину? Разве я не могу просто объехать ее? Это и позволяет нам делать асинхронный JavaScript. Он создает для нас еще одну «полосу». Асинхронность означает, что если JavaScript должен дождаться завершения операции, он выполнит остальную часть кода во время ожидания. Мы можем переместить наши действия с основной линии и выполнять их в своем собственном темпе, позволяя им заниматься своими делами. И как мы этого добиваемся? Используя обратные вызовы, промисы и async/await.

Функции обратного вызова (callback)

Обратные вызовы — это функции, вложенные в другую функцию в качестве аргумента. Их можно использовать как часть синхронного или асинхронного кода. Синхронный обратный вызов выполняется во время выполнения функции высшего порядка, которая использует обратный вызов.
function startFirst(car, callback) {
  console.log("Start " + car);
  callback();
}

// функция обратного вызова
function startSecond() {
  console.log("Start second car");
}

// передача функции как аргумент
startFirst("first car", startSecond);
Результат выполнения кода.
Start first car
Start second car
Мы также можем сделать обратные вызовы частью асинхронного JavaScript. Асинхронный обратный вызов выполняется после выполнения функции высшего порядка, которая использует колбэк. Если наша машина сломается, мы отвезем ее к механику, после чего снова сможем ею пользоваться. Сначала нам нужно подождать некоторое время, чтобы починить машину. Смоделируем ожидание с помощью setTimeout, а затем мы сможем наслаждаться вождением нашей только что починенной машины.
function fixMyCar(car) {
  setTimeout(() => {
    console.log(`Fixing your ${car}.`);
  }, 1000);
}

function driveMyCar(car) {
  console.log(`Driving my new ${car}.`);
}

let myCar = "BMW x5";

fixMyCar(myCar);
driveMyCar(myCar);
Результат выполнения кода.
Driving my new BMW x5.
Fixing your BMW x5.
JavaScript сначала выполнил синхронный код (в нашем случае вызов функции driveMyCar()), а затем через 1000 миллисекунд записал результат fixMyCar(). Но как мы можем водить машину, если она еще не починена? Мы должны передать функцию driveMyCar() в качестве колбэка функции fixMyCar(). Таким образом, функция driveMyCar() не будет выполняться до тех пор, пока автомобиль не будет отремонтирован.
function fixMyCar(car, callback) {
  setTimeout(() => {
    console.log(`Fixing your ${car}.`);
    callback(car);
  }, 1000);
}

function driveMyCar(car) {
  console.log(`Driving my new ${car}.`);
}

let myCar = "BMW x5";

fixMyCar(myCar, driveMyCar);
Результат выполнения кода.
Fixing your BMW x5.
Driving my new BMW x5.
Что ж, отлично, мы починили нашу машину и теперь можем на ней ездить. Но что, если нашу машину нельзя починить? Как мы будем обрабатывать ошибки? А как насчет ремонта нескольких автомобилей каждый день? Давайте посмотрим на это в действии.
function fixMyCar(car, success, failure) {
  setTimeout(() => {
    car ? success(car) : failure(car);
  }, 1000);
}

const car1 = "BMW x5";
const car2 = "Toyota RAV4";
const car3 = "Honda Civic";

fixMyCar(
  car1,
  function (car1) {
    console.log(`Fixed your ${car1}.`);
    fixMyCar(
      car2,
      function (car2) {
        console.log(`Fixed your ${car2}.`);
        fixMyCar(
          car3,
          function (car3) {
            console.log(`Fixed your ${car3}.`);
          },
          function (car3) {
            console.log(`Your ${car3} car can not be fixed.`);
          }
        );
      },
      function (car2) {
        console.log(`Your ${car2} car can not be fixed.`);
      }
    );
  },
  function (car1) {
    console.log(`Your ${car1} car can not be fixed.`);
  }
);
Результат выполнения кода.
Fixed your BMW x5.
Fixed your Toyota RAV4.
Fixed your Honda Civic.
У вас кружится голова, пытаясь понять это? Не волнуйтесь, вы не одиноки. Есть причина, по которой это явление называется callback hell. Кроме того, обратите внимание, что если одна из машин разбита, то есть ее не получается определить, другие машины даже не получат шанса на ремонт.
function fixMyCar(car, success, failure) {
  setTimeout(() => {
    car ? success(car) : failure(car);
  }, 1000);
}

const car1 = "BMW x5";
const car2 = undefined;
const car3 = "Honda Civic";

fixMyCar(
  car1,
  function (car1) {
    console.log(`Fixing your ${car1}.`);
    fixMyCar(
      car2,
      function (car2) {
        console.log(`Fixing your ${car2}.`);
        fixMyCar(
          car3,
          function (car3) {
            console.log(`Fixing your ${car3}.`);
          },
          function (car3) {
            console.log(`Your ${car3} car can not be fixed.`);
          }
        );
      },
      function (car2) {
        console.log(`Your ${car2} car can not be fixed.`);
      }
    );
  },
  function (car1) {
    console.log(`Your ${car1} car can not be fixed.`);
  }
);
Результат выполнения кода.
Fixing your BMWx5.
Your undefined car can not be fixed.

Промисы (Promise)

Promise — это объект, который можно использовать для получения результата асинхронной операции, когда этот результат недоступен прямо сейчас. Поскольку код JavaScript выполняется неблокирующим образом, промисы становятся необходимыми, когда нам нужно дождаться какой-либо асинхронной операции, не задерживая выполнение остального кода. Промисы JavaScript — это объект, который может находиться в одном из трех состояний.
  • Pending (в ожидании) - обещание еще не выполнено (ваша машина у механика)
  • Fulfilled (выполнено) - запрос выполнен успешно (автомобиль отремонтирован)
  • Rejected (отклонено) - запрос не выполнен (автомобиль не может быть починен)
Чтобы создать промис в JavaScript, используйте ключевое слово new и внутри конструктора передайте функцию-исполнитель. Затем эта функция отвечает за разрешение или отклонение промиса. Давайте представим следующий сценарий. Если нашу машину починят, то мы сможем отправиться в отпуск. Там мы можем осмотреть достопримечательности, затем мы можем сделать несколько снимков, опубликовать их в социальных сетях. Но если машину нельзя починить, то придется остаться дома. Давайте напишем наши шаги.
  • Механик обещает, что починит нашу машину
  • Починить машину — значит отправиться в отпуск.
  • Оказавшись там, мы можем отправиться на экскурсию
  • Сделаем несколько фотографий
  • После этого мы опубликуем их в социальных сетях
Используем setTimeout для имитации асинхронности.
const mechanicsPromise = new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      const fixed = true;
      if (fixed) resolve("Car is fixed");
      else reject("Car can not be fixed");
    }, 2000);
});

console.log(mechanicsPromise);
Результат выполнения кода.
Promise { <pending> }

[[Prototype]]: Promise
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined
Но почему PromiseResult - undefined? Разве ваш механик не говорил вам, что попытается починить вашу машину? Нет, ваш механик вас не обманывал. Что мы забыли сделать, так это обработать промис. И как мы это делаем? Используя методы .then() и .catch().
const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed");
    else reject("Car can not be fixed. Go home");
  }, 2000);
});

mechanicsPromise
  .then((message) => {
    console.log(`Success: ${message}`);
  })
  .catch((error) => {
    console.log(error);
  });
Результат выполнения кода.
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Success: Car is fixed
Как видно из блока кода выше, мы используем .then() для получения результата метода resolve() и .catch() для получения результата метода reject(). Наша машина починена, и теперь мы можем отправиться в отпуск и сделать все, что планировали. Метод .then() возвращает новое промис с результатом, преобразованным в значение. Мы можем вызвать метод .then() для возвращенного промиса следующим образом:
const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed");
    else reject("Car can not be fixed");
  }, 2000);
});

mechanicsPromise
  .then((message) => {
    console.log(`Success: ${message}`);
    message = "Go sight seeing";
    return message;
  })
  .then((message) => {
    console.log(message);
    message = "Take some pictures";
    return message;
  })
  .then((message) => {
    console.log(message);
    message = "Posting pictures on social media";
    console.log(message);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("Go home");
  });
Результат выполнения кода.
Success: Car is fixed
Go sight seeing
Take some pictures
Posting pictures on social media
Go home
Как вы можете видеть, после каждого вызова метода .then() мы вызывали еще один .then() с сообщением от предыдущего .then(). Мы также добавили .catch() для обнаружения любых ошибок, которые могут возникнуть. Если мы поедем или не поедем в отпуск, нам обязательно придется вернуться домой. Это то, что делает .finally(), этот метод всегда выполняется независимо от того, выполнено ли обещание или отклонено. Другими словами, метод .finally() выполняется, когда обещание (promise) выполнено. Наш код выглядит немного лучше, чем когда мы использовали колбэки. Но мы можем сделать это еще лучше с помощью специального синтаксиса под названием async/await. Это позволяет нам работать с промисами более удобным способом.

async/await

async/await позволяет нам писать промисы, но код будет выглядеть синхронным, хотя на самом деле он асинхронный. Под капотом мы все еще используем Promise. async/await — это синтаксический сахар, а это означает, что хотя он и не добавляет к нашему коду никаких новых функций, его приятнее использовать.
const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed");
    else reject("Car can not be fixed");
  }, 2000);
});

async function doMyThing() {
  let message = await mechanicsPromise;
  console.log(`Success: ${message}`);

  message = "Go sight seeing";
  console.log(message);

  message = "Take some pictures";
  console.log(message);

  message = "Posting pictures on social media";
  console.log(message);
  console.log("Go home");
}

doMyThing()
Результат выполнения кода.
Success: Car is fixed
Go sight seeing
Take some pictures
Posting pictures on social media
Go home
Как видите, ключевое слово await заставляет функцию приостанавливать выполнение и ждать разрешенного промиса, прежде чем оно продолжится. Ключевое слово await можно использовать только внутри асинхронной функции. А что, если машина сломается? Как мне обрабатывать ошибки с этим новым синтаксисом? Мы можем использовать блок try/catch.
const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = false;
    if (fixed) resolve("Car is fixed");
    else reject("Car can not be fixed");
  }, 2000);
});

async function doMyThing() {
  try {
    let message = await mechanicsPromise;
    console.log(`Success: ${message}`);

    message = "Go sight seeing";
    console.log(message);

    message = "Take some pictures";
    console.log(message);

    message = "Posting pictures on social media";
    console.log(message);
    console.log("Go home");
  } catch (error) {
    console.log(error);
  }
}

doMyThing();
Результат выполнения кода.
Your car can not be fixed
Используйте блок try/catch только в том случае, если вызов помечен как await. В противном случае исключение не будет поймано.