Разбираемся с промисами в JavaScript

Автор: admin от 11-02-2019, 06:10, посмотрело: 15

Доброго времени суток, Хабр! Представляю вашему вниманию перевод статьи «Understanding Promises in javascript» автора Sukhjinder Arora.



Разбираемся с промисами в JavaScript
Ссылка на перевод статьи по асинхронному javascript от этого же автора.[/i]



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



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



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



getData(function(x){
    console.log(x);
    getMoreData(x, function(y){
        console.log(y); 
        getSomeMoreData(y, function(z){ 
            console.log(z);
        });
    });
});


Здесь я запрашиваю некоторые данные с сервера при помощи функции [i]getData()[/i], которая получает данные внутри функции обратного вызова. Внутри функции обратного вызова я запрашиваю дополнительные данные при помощи вызова функции [i]getMoreData()[/i], передавая предыдущие данные как аргумент и так далее.



Это то, что мы называем “адом обратных вызовов”, где каждый обратный вызов вложен внутрь другого, и каждый внутренний обратный вызов зависит от его родителя.



Мы можем переписать приведенный выше фрагмент используя промисы:



getData()
  .then((x) => {
    console.log(x);
    return getMoreData(x);
  })
  .then((y) => {
    console.log(y);
    return getSomeMoreData(y);
  })
  .then((z) => {
    console.log(z);
   });


Вы можете видеть, что стало более читабельно, чем в случае первого примера с обратными вызовами.



Что такое Промисы?



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



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



Состояния промисов



Промис в javascript, как и обещание в реальной жизни, имеет 3 состояния. Это может быть 1) нерешенный(в ожидании), 2) решенный/resolved (выполненный) или 3) отклоненный/rejected.



Разбираемся с промисами в JavaScript


Нерешенный или Ожидающий — Промис ожидает, если результат не готов. То есть, ожидает завершение чего-либо(например, завершения асинхронной операции).

Решенный или Выполненный — Промис решен, если результат доступен. То есть, что-то завершило свое выполнение(например, асинхронная операция) и все прошло хорошо.

Отклоненный — Промиc отклонен, если произошла ошибка в процессе выполнения.



Теперь мы знаем, что такое Промис и его терминологию, давайте вернемся назад к практической части промисов.



Создаем Промис



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



Синтаксис:



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


Мы создали новый промис, используя конструктор Промисов, он принимает один аргумент, обратный вызов, также известный как исполнительная функция, которая принимает 2 обратных вызова, [i]resolve[/i] и [i]reject[/i].



Исполнительная функция выполняется сразу же после создания промиса. Промис становится выполненным при помощи вызова [i]resolve()[/i], а отклоненным при помощи [i]reject()[/i]. Например:



const promise = new Promise((resolve, reject) => {
  if(allWentWell) {
    resolve('Все прошло отлично!');
  } else {
    reject('Что-то пошло не так');
  }
});


[i]resolve()[/i] и [i]reject()[/i] принимают один аргумент, который может быть строкой, числом, логическим выражением, массивом или объектом.



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



const promise = new Promise((resolve, reject) => {
  const randomNumber = Math.random();
setTimeout(() => {
    if(randomNumber < .6) {
      resolve('Все прошло отлично!');
    } else {
      reject('Что-то пошло не так');
  }
  }, 2000);
});


Здесь я создал новый промис используя конструктор Промисов. Промис выполняется или отклоняется через 2 секунды после его создания. Промис выполняется, если [i]randomNumber[/i] меньше, чем .6 и отклоняется в остальных случаях.



Когда промис был создан, он будет в состоянии ожидания и его значение будет [i]undefined[/i].



Разбираемся с промисами в JavaScript

После 2 секунд таймер заканчивается, промис случайным образом либо выполняется, либо отклоняется, и его значением будет то, которое передано в функцию [i]resolve[/i] или [i]reject[/i]. Ниже пример двух случаев:



Успешное выполнение:



Разбираемся с промисами в JavaScript

Отклонение промиса:



Разбираемся с промисами в JavaScript

Примечание: Промис может быть выполнен или отклонен только один раз. Дальнейшие вызовы [i]resolve()[/i] или [i]reject()[/i] никак не повлияют на состояние промиса. Пример:



const promise = new Promise((resolve, reject) => {
  resolve('Promise resolved');  // Промис выполнен
  reject('Promise rejected');   // Промис уже не может быть отклонен
});


Так как [i]resolve()[/i] была вызвана первой, то промис теперь получается статус “выполненный”. Последующий вызов [i]reject()[/i] никак не повлияет на состояние промиса.



Использование Промиса



Теперь мы знаем как создавать промисы, давайте теперь разберемся как применять уже созданный промис. Мы используем промисы при помощи методов [i]then()[/i] и [i]catch()[/i].



Например, запрос данных из API при помощи [i]fetch[/i], которая возвращает промис.



[i].then()[/i] синтаксис: [i]promise.then(successCallback, failureCallback)[/i]



[i]successCallback[/i] вызывается, если промис был успешно выполнен. Принимает один аргумент, который является значением переданным в [i]resolve()[/i].



[i]failureCallback[/i] вызывается, если промис был отклонен. Принимает один аргумент, который является значением преданным в [i]reject()[/i].



Пример:



const promise = new Promise((resolve, reject) => {
  const randomNumber = Math.random();
  
  if(randomNumber < .7) {
    resolve('Все прошло отлично!');
  } else {
    reject(new Error('Что-то пошло не так'));
  }
});
promise.then((data) => {
  console.log(data);  // вывести 'Все прошло отлично!'
  },
  (error) => {
  console.log(error); // вывести ошибку
  }
);


Если промис был выполнен, то вызывается [i]successCallback[/i] со значением, переданным в [i]resolve()[/i]. И если промис был отклонен, то вызывается [i]failureCallback[/i] со значением, переданным в reject().



[i].catch()[/i] синтаксис: [i]promise.catch(failureCallback)[/i]



Мы используем [i]catch()[/i] для обработки ошибок. Это более читабельно, нежели обработка ошибок внутри [i]failureCallback[/i] внутри обратного вызова метода [i]then()[/i].



const promise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
promise
  .then((data) => {
     console.log(data);
   })
  .catch((error) => {
     console.log(error); // вывести ошибку
  });


Цепочка промисов



Методы [i]then()[/i] и [i]catch()[/i] также могут возвращать новый промис, который может быть обработан цепочкой других then() в конце предыдущего метода then().



Мы используем цепочку промисов, когда хотим выполнить последовательность промисов.



Например:



const promise1 = new Promise((resolve, reject) => {
  resolve('Promise1 выполнен');
});
const promise2 = new Promise((resolve, reject) => {
  resolve('Promise2 выполнен');
});
const promise3 = new Promise((resolve, reject) => {
  reject('Promise3 отклонен');
});
promise1
  .then((data) => {
    console.log(data);  // Promise1 выполнен
    return promise2;
  })
  .then((data) => {
    console.log(data);  // Promise2 выполнен
    return promise3;
  })
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);  // Promise3 отклонен
  });


И так, что тут происходит?



Когда [i]promise1[/i] выполнен, вызывается метод [i]then(),[/i] который возвращает promise2.

Далее, когда выполнен [i]promise2[/i], снова вызывается [i]then()[/i] и возвращает [i]promise3[/i].



Так как promise3 отклонен, вместо следующего [i]then()[/i], вызывается [i]catch()[/i], который и обрабатывает отклонение [i]promise3[/i].



Примечание: Как правило достаточно одного метода [i]catch()[/i] для обработки отклонения любого из промисов в цепочке, если этот метод находится в конце неё.



Распространенная ошибка



Достаточно много новичков делают ошибку, вкладывая одни промисы внутрь других. Например:



const promise1 = new Promise((resolve, reject) => {
  resolve('Promise1 выполнен');
});
const promise2 = new Promise((resolve, reject) => {
  resolve('Promise2 выполнен);
});
const promise3 = new Promise((resolve, reject) => {
  reject('Promise3 отклонен');
});
promise1.then((data) => {
  console.log(data);  // Promise1 выполнен
  promise2.then((data) => {
    console.log(data);  // Promise2 выполнен
    
    promise3.then((data) => {
      console.log(data);
    }).catch((error) => {
      console.log(error);  // Promise3 отклонен
    });
  }).catch((error) => {
    console.log(error);
  })
}).catch((error) => {
    console.log(error);
  });


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



Promise.all( )



Этот метод берет массив промисов и возвращает новый промис, который будет выполненным, когда все промисы внутри массива выполнены или отклонен, как только встречается промис, который отклоняется. Например:



const promise1 = new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve('Promise1 выполнен');
 }, 2000);
});
const promise2 = new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve('Promise2 выполнен');
 }, 1500);
});
Promise.all([promise1, promise2])
  .then((data) => console.log(data[0], data[1]))
  .catch((error) => console.log(error));


Здесь аргументом внутри [i]then()[/i] выступает массив, который содержит значения промисов в том же порядке, в котором они передавались в [i]Promise.all()[/i].(Только в том случае, если все промисы выполняются)



Промис отклоняется с причиной отклонения первого промиса в переданном массиве. Например:



const promise1 = new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve('Promise1 resolved');
 }, 2000);
});
const promise2 = new Promise((resolve, reject) => {
 setTimeout(() => {
  reject('Promise2 rejected');
 }, 1500);
});
Promise.all([promise1, promise2])
  .then((data) => console.log(data[0], data[1]))
  .catch((error) => console.log(error));  // Promise2 отклонен


Здесь у нас есть два промиса, где один выполняется через 2 секунды, а другой отклоняется через 1.5 секунды. Как только второй промис отклоняется, возвращенный от [i]Promise.all()[/i] промис отклоняется не дожидаясь выполнения первого.



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



По итогу мы имеем [i]Promise.all()[/i], который ждет успешное выполнение всех промисов, либо завершает свое выполнение при обнаружении первой неудачи в массиве промисов.



Promise.race( )



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



const promise1 = new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve('Promise1 resolved');
 }, 1000);
});
const promise2 = new Promise((resolve, reject) => {
 setTimeout(() => {
  reject('Promise2 rejected');
 }, 1500);
});
Promise.race([promise1, promise2])
  .then((data) => console.log(data))  // Promise1 выполнен
  .catch((error) => console.log(error));


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



Здесь [i]data[/i], которая передается в [i]then()[/i] является значением первого, выполненного, промиса.



По итогу, [i]Promise.race()[/i] дожидается первого промиса и берет его статус как статус возвращаемого промиса.



[i]Комментарий автора перевода: Отсюда собственно и название. Race — гонка[/i]



Заключение



Мы узнали, что такое промисы и с чем их едят в javascript. Промисы состоят из двух частей 1) Создание промиса и 2) Использование промиса. Большую часть времени вы будете пользоваться промисами, нежели создавать их, но важно знать как они создаются.



Вот и все, надеюсь эта статья оказалась для вас полезной!


Источник: Хабр / Интересные публикации

Категория: Веб-разработка

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

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *