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

Promise (промис, англ. "обещание") - это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Так как большенство людей пользуются уже созданными промисами, это руководство начнем с объяснения использования вернувшихся промисов до объяснения принципов создания. 

В сущности, промис - это возвращаемый объект, в который вы записываете два коллбэка вместо того, чтобы передать их функции.

Например, вместо старомодной функции, которая принимает два коллбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:

function doSomethingOldStyle(successCallback, failureCallback) {
  console.log("It is done.");
  // Succeed half of the time.
  if (Math.random() > .5) {
    successCallback("SUCCESS")
  } else {
    failureCallback("FAILURE")
  }
}

function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomethingOldStyle(successCallback, failureCallback);

…современные функции возвращают промис, в который вы записываете ваши коллбэки:

function doSomething() {
  return new Promise((resolve, reject) => {
    console.log("It is done.");
    // Succeed half of the time.
    if (Math.random() > .5) {
      resolve("SUCCESS")
    } else {
      reject("FAILURE")
    }
  })
}

const promise = doSomething(); 
promise.then(successCallback, failureCallback);

…или просто:

doSomething().then(successCallback, failureCallback);

Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.

Гарантии

В отличие от старомодных переданных коллбэков промис дает некоторые гарантии:

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

Но наиболее непосредственная польза от промисов - цепочка вызовов (chaining).

Цепочка вызовов

Общая нужда - выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (promise chain).

Вот в чём магия: функция then возвращает новый промис, отличяющийся от первоначального:

let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);

или

let promise2 = doSomething().then(successCallback, failureCallback);

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

По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.

Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической "Вавилонской башне" коллбэков:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Итоговый результат: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

В современных функциях мы записываем коллбэки в возвращаемые промисы - формируем цепочку промисов:

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Итоговый результат: ' + finalResult);
})
.catch(failureCallback);

Аргументы then необязательны, а catch(failureCallback) - это сокращение для then(null, failureCallback). Вот как это выражено с помощью стрелочных функций:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Итоговый результат: ${finalResult}`);
})
.catch(failureCallback);

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

 

Цепочка вызовов после catch

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

new Promise((resolve, reject) => {
    console.log('Начало');

    resolve();
})
.then(() => {
    throw new Error('Где-то произошла ошибка');
        
    console.log('Выведи это');
})
.catch(() => {
    console.log('Выведи то');
})
.then(() => {
    console.log('Выведи это, несмотря ни на что');
});

В результате выведется данный текст:

Начало
Выведи то
Выведи это, несмотря ни на что

Заметьте что текст "Выведи это" не вывелся потому что "Где то произошла ошибка" привела к отказу

 

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

Вы могли ранее заметить, что failureCallback  повторяется три раза  в "pyramid of doom", а в цепочке промисов всего лишь один раз:

 

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Итоговый результат: ${finalResult}`))
.catch(failureCallback);

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

 

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Итоговый результат: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async/await в ECMAScript 2017:

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Итоговый результат: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

Работа данного кода основана на промисах. Для примера здесь используется функция doSomething(), которая встречалась ранее. Вы можете прочитать больше о синтаксисе здесь

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

 

Создание промиса вокруг старого коллбэка

Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.

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

setTimeout(() => saySomething("10 seconds passed"), 10000);

Смешивание старого коллбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.

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

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

В сущьности, конструктор промиса становится исполнителем функции, который позволяет нам резолвить или режектить промис вручную. Так как setTimeout всегда имеет успех, мы опустили reject в этом случае.

 

Композиция

Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклоненные промисы соответственно. Это иногда бывает полезно.

Promise.all() и Promise.race() - два метода запустить асинхронные операции параллельно.

Последовательное выполнение композиции возможно при помощи хитрости JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);

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

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync функция примет любое количество функций в качестве аргументов и вернет новую функцию которая проимет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными либо синхронными, и они гарантированно выполнятся в правильной последовательности:

const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

In ECMAScript 2017, последовательные композиции могут быть выполненны более простым способом с помощью async/await:

for (const f of [func1, func2]) {
  await f();
}

 

 

Порядок выполнения

Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызванны синхронно, даже с уже разрешенным промисом:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой  в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

 

 

Смотрите также

Метки документа и участники

Внесли вклад в эту страницу: kefir266, yojeek, djigach, Airomad, winexy, Geloosa
Обновлялась последний раз: kefir266,