Обещания были изобретены в середине-конце 1970-х годов в Университете Индианы в США. Это языковые конструкции, используемые для синхронизации выполнения в средах параллельного программирования.

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

Обещания также значительно упрощают работу с ошибками.

JavaScript широко использует асинхронный код из-за блокирующего характера его реализации однопоточного цикла событий.

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

Продолжение-проходной стиль

Так был написан асинхронный JavaScript до повторного открытия обещаний сообществом JavaScript в середине 2000-х годов.

function foo(arg1, done) {
  setTimeout(() => { // Simulate some async calculation
    done(arg1 + 1);
   });
}
function bar(arg1) {
  console.log(arg1); // 2
}
foo(1, bar);

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

Прямой стиль

function foo(arg1, done) {
  return new Promise(resolve => setTimeout(() => resolve(arg1 + 1)); 
}
function bar(arg1) {
  console.log(arg1); // 2
}
function fail(err) {
  console.log(err);
}
foo(1)
  .then(bar)
  .catch(fail);

Обратите внимание, как промисы упрощают обработку ошибок и как API асинхронной функции `foo` становится проще следовать с помощью` then` и `catch`.

Встроенный объект обещания

ES2015 включает в себя встроенный объект-функцию Promise. IE11 не поддерживает этот объект, поэтому вам следует использовать полифил, например es6-prom.

Некоторые библиотеки и языки делают различие между фьючерсами, отложенными и обещаниями. TC-39 (комитет по стандартизации для JavaScript) сознательно решил избежать этой сложности, и ES2015 не делает различий между этими типами конструкций.

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

По сути, обещание - это API-интерфейс, обертывающий конечный автомат, контролирующий доступ к будущему значению. Состояния конечного автомата: ожидает, выполнено и отклонено. Вы можете перейти только от ожидающих к выполненным или отклоненным, но никак иначе.

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

  • решено со значением, которое приводит к выполнению, или,
  • решено с отклоненным обещанием или,
  • решено с ожидающим обещанием

Решенное состояние ожидания означает, что он ожидает конечного результата некоторого другого обещания.

Пример

Следующее создает новое обещание, которое разрешается с помощью строки `foo` примерно через пять секунд.

new Promise(resolve => {
  setTimeout(() => resolve('foo'), 5000);
});

API

API для объектов обещаний в ES2015 имеет конструктор и четыре важных метода: then, catch, all и race.

Некоторые библиотеки обещаний реализуют отмену для отменяемых обещаний. ES2015 не имеет этого, и хотя он был на радаре TC-39 для будущего включения, теперь он выглядит так, как будто его убили. Тем не менее, отменяемые обещания в настоящее время могут быть реализованы без особых хлопот (см. Пример внизу этого сообщения).

Конструктор

Функция конструктора `Promise` используется для« обещания »функций. Он принимает, принимает одну функцию. Эта функция называется функцией-исполнителем и выполняется синхронно и немедленно.

So

new Promise(resolve => {
  console.log('executor');
  resolve();
});
console.log('line after promise');

… Выходы (обратите внимание на порядок)

executor
line after promise

Функция-исполнитель имеет два аргумента обратного вызова: «resolve» и «reject» в указанном порядке. Их можно вызвать, чтобы разрешить или отклонить обещание.

Также существуют сокращения для разрешения и отклонения обещания:

Promise.resolve()

и

Promise.reject()

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

Promise.resolve('foo')
  .then(result => console.log(result)); // Prints 'foo'

тогда

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

then принимает два необязательных обратных вызова onFulfilled и onRejected. Последний используется реже.

onFulfilled вызывается со значением выполнения обещания, которое было вызвано then.

onRejected вызывается с указанием причины отклонения обещания then.

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

Это означает, что обратные вызовы, передаваемые в `then`, всегда будут выполняться асинхронно.

Микрозадачи гарантированно будут выполнены перед следующей макрозадачей.

Это означает, что обратный вызов `then` будет запущен перед следующей макрозадачей в очереди заданий, но после любого синхронного кода.

So

setTimeout(() => console.log('setTimeout1'));
new Promise(resolve => {
  console.log('executor function');
  setTimeout(() => console.log('setTimeout2'))
  resolve('result');
})
.then(result => console.log(result));
console.log(‘line after promise chain’);

… Выходы

executor function
line after promise chain
result
setTimeout1
setTimeout2

Важно отметить, что если функция, переданная в `then`, возвращает обещание, то цепочка будет ждать разрешения или отклонения этого обещания.

поймать

Этот метод обеспечивает удобную обработку ошибок. Он принимает один обратный вызов, который будет вызываться, если внутри возникнет исключение или цепочка обещаний будет отклонена. Он имеет асинхронную природу, аналогичную then. Обратный вызов предоставляется с аргументом, соответствующим вызванному исключению или значению, предоставленному для вызова `reject`.

setTimeout(() => console.log('setTimeout1'))
new Promise((resolve, reject) => {
  console.log('executor function');
  setTimeout(() => console.log('setTimeout2'))
  reject('error');
})
.catch(error => console.log(error));
console.log('line after promise chain');

… Выходы:

executor function
line after promise chain
error
setTimeout1
setTimeout2

Помните, что `catch` остановит распространение ошибки, если только она не будет явно выдана повторно.

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

new Promise((resolve, reject) => {
  throw 'an error occurred'
})
.catch(error => console.log(error))
.then(() => console.log('we are good (as far as I know!)'));

… Выходы:

an error occurred
we are good (as far as I know!)

Взаимодействие с синхронными обработчиками исключений

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

try {
  new Promise((resolve, reject) => {
    throw 'an error occurred'
  })
  .catch(error => {
   console.log(error);
    throw error;
  })
  .then(() => console.log('we are good as far as I know!'));
} catch (err) {
  console.log('error caught:', err); // never called here!
}

… Выводит (обратите внимание, что «ошибка обнаружена» не выводится):

an error occurred

Также обратите внимание, что асинхронная функция имеет два API - API синхронных исключений и API на основе асинхронных обещаний.

Если вам не нужно использовать конструктор `Promise` для обещания функции, вы можете использовать вышеупомянутый` Promise.resolve`, чтобы избежать необходимости обслуживать синхронные исключения и предоставить вашей функции последовательный API, основанный на обещаниях.

return Promise.resolve()
  .then(() => {
    /* put your code in here */
  })
  .catch(error => { 
    console.log(error);
    throw error;
  });

… Теперь все ваши исключения попадут в цепочку обещаний.

все

Функция all позволяет всем поставляемым асинхронным функциям работать параллельно. `all` разрешится только тогда, когда разрешатся все (да!) функции, предоставленные в массиве (или другом итеративном). Он отклонит, как только одна из предоставленных функций отклонит или выдаст исключение.

Следующий код немедленно отклонит внешнюю цепочку обещаний и напечатает `error`.

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

var p1 = new Promise(resolve => setTimeout(() => resolve('foo')))
  .then(result => console.log(result));
var p2 = Promise.reject('error');
Promise.all([p1, p2])
  .then(() => console.log('all done'))
  .catch(error => console.log(error));

гонка

`race` разрешится с первым разрешенным результатом массива (или другого итерируемого) обещаний. Даже если последующее обещание будет отклонено. Это можно использовать для таких вещей, как обработка тайм-аутов сети.

Далее будет напечатано «все готово», и отклонение будет проигнорировано.

var work = new Promise(resolve => setTimeout(() => resolve('foo'), 100))
var timeout = new Promise((_, reject) => setTimeout(() => reject('a timeout occurred'), 200))
Promise.race([work, timeout])
  .then(() => console.log('all done'))
  .catch(error => console.log(error));

Следующее (как указано выше, но с инвертированными таймингами) напечатает «время ожидания истекло», и обработанное обещание будет проигнорировано.

var work = new Promise(resolve => setTimeout(() => resolve('foo'), 200))
var timeout = new Promise((_, reject) => setTimeout(() => reject('a timeout occurred'), 100))
Promise.race([work, timeout])
  .then(() => console.log('all done'))
  .catch(error => console.log(error));

Примечания по использованию

Работая с обещаниями, вы почти всегда должны их возвращать.

Избегайте такого кода:

function doSomethingAsync() {
  return new Promise(resolve => /* whatever */);
}
function foo() {
  doSomethingAsync(); // You should return the promise!
}
foo();

Не возвращая обещание, `foo` предотвращает возможность клиентского кода контролировать, дополнять или отвечать на цепочку обещаний.

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

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

Вы почти НИКОГДА не должны открывать обратные вызовы `resolve` и` reject` вне функции исполнителя.

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

Реализация отменяемого обещания

Несмотря на то, что JavaScript не предоставляет встроенный механизм для отмены обещания, мы можем написать свой собственный.

const CANCELLED = Symbol('cancelled');
function cancellable(promise) {
  let isCancelled = false;
  const wrapper = new Promise((resolve, reject) => {
    promise.then(val => isCancelled ? reject(CANCELLED) : resolve(val));
    promise.catch(error => isCancelled ? reject(CANCELLED) : reject(error));
  });
  return {
    promise: wrapper,
    cancel() {
      isCancelled = true;
    },
  };
}
cancellable.CANCELLED = CANCELLED;
export default cancellable;

Функции генератора и доходность

ES2015 содержит функции генератора и контекстное ключевое слово yield.

Они позволяют одновременно приостанавливать и возобновлять оценку функции.

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

Чтобы добиться этого, нам нужно обернуть наш код функцией генератора (назовем ее `asynch`), чтобы мы могли воспользоваться контекстным ключевым словом` yield` и промежуточной функцией возврата / возобновления.

Например:

asynch(function*() {
  const result1 = yield anAsyncFunction();
  const result2 = yield anotherAsyncFunction();
  console.log(result1 + result2);
});

… И функция `asynch` может выглядеть примерно так:

function asynch(gFn) {
  var g = gFn();
  return Promise.resolve().then(go);
  function go() {
    var result = g.next(…arguments);
    if (isPromise(result.value)) {
      return result.value.then(go);
    }
    if (Array.isArray(result.value)) {
      return Promise.all(result.value).then(go);
    }
    if (isObject(result.value)) {
      var o = {};
      var promises = 
        Object.keys(result.value)
          .map(k=>result.value[k].then(r=>o[k] = r));
      return Promise.all(promises).then(()=>o).then(go);
    }
    return Promise.resolve(result);
  }
}

Возможности этого шаблона были признаны сообществом и TC-39. Планируется, что стандартизированная реализация шаблона будет включена в будущую версию языка с двумя новыми ключевыми словами: `async` (эффективно выполняет то, что делает` asynch` выше) и `await` (аналогично использованию` yield` выше ).

Меня зовут Бен Астон, и спасибо, что прочитали мой пост сегодня. Я работаю консультантом по JavaScript из Лондона, и я доступен для краткосрочных консультаций по всему миру. Со мной можно связаться по адресу [email protected].

Если вам понравился этот материал, дайте мне знать, нажав кнопку «рекомендовать» ниже.

Также вас может заинтересовать мой пост о замыканиях в JavaScript.