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

someAsyncCall.then(res => res.data).then();

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

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

Я буду ссылаться на успешные и неудачные обратные вызовы, установленные на then, на onResolved и onRejected на протяжении всего написания.

Код ниже будет использован для иллюстрации.

const delJSON = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (url) {
        resolve({
          message: 'deleted',
          id: 2
        });
      } else {
        reject({
          error: 'delJSON failed'
        });
      }
    }, 500);
  });
};
const getJSON = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (url) {
        resolve({
          data: [1, 2],
          id: 1
        });
      } else {
        reject({
          error: 'getJSON failed'
        });
      }
    }, 2000);
  });
};

Основы

Три состояния обещания

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

  1. Ожидание: исходное состояние
  2. Решено: операция выполнена успешно.
  3. Отклонено: операция не удалась

Единственный способ, которым Ожидает превратиться в Решено или Отклонено, - это позвонить по телефону resolve(value) или reject(error).

После изменения состояния вы не сможете вернуться к Pending. Он останется навсегда.

Инициализация обещания

Вызов конструктора Promise для возврата экземпляра.

promise.then(function(data) {
  // success
}, function(error) {
  // failure
});

Конструктор обещания принимает функцию в качестве единственного аргумента, который имеет resolve и reject в качестве параметров функции. Вызов resolve со значением вызовет onResolved, который устанавливается путем вызова then в экземпляре Promise. Точно так же вызов reject с ошибкой вызовет onRejected.

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

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

let promise = new Promise(function(resolve, reject) {
  console.log('start');
  resolve();
});
promise.then(function() {
  console.log('done');
});
console.log('ok');
// start
// ok
// done

потом

Все экземпляры обещаний имеют then метод, который используется для добавления двух обратных вызовов: onResolved и onRejected.

const onResolved = (data) => {return data.id};
const onRejected = (error) => {};
promise.then(onResolved, onRejected).then(id => console.log(id));

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

Ловить

catch - это сахарный синтаксис для .then(null, onRejected), который запускается при возникновении ошибки.

getJSON().then(data => {
}).catch(err => {
  // {error: 'getJSON failed'}
});

Все

Вы можете использовать Promise.all для выполнения нескольких операций.

const p = Promise.all([p1, p2, p3]);

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

Давайте сначала посмотрим, как мы разрешаем p.

По сути, для решения p необходимо решить p1,p2,p3 все.

const p = Promise.all([getJSON('baidu.com'), delJSON('google.com')]);
p.then(data => data).catch(err => console.log(err));
// [{data: [1,2], id: 1}, {message: 'deleted', id: 2}]

Как видите, разрешенные значения будут переданы onResolved в том же порядке, в котором мы создаем все экземпляры обещаний.

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

const p = Promise.all([getJSON('baidu.com'), delJSON()]);
p.then(data => console.log(data)).catch(err => console.log(err));
// {error: 'delJSON failed'}

Гонка

Это говорит само за себя - первое resolved или rejected обещание будет определять состояние экземпляра обещания, созданного race.

const p = Promise.race([getJSON('baidu.com'), delJSON('google.com')]);
p.then(data => console.log(data)).catch(err => console.log(err));
// { message: 'deleted', id: 2 }

Передовой

Разрешить с помощью другого экземпляра обещания

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

getJSON('baidu.com').then(data => {
  console.log(data); // not called
}).catch(err => {
  console.log('the error ', err);
  // {error: 'operation failed'}
});
const delJSON = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (url) {
        resolve({
          message: 'deleted',
          id: 2
        });
      } else {
        reject({
          error: 'delJSON failed'
        });
      }
    }, 500);
  });
};

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

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

Вы можете задаться вопросом, какой способ обработки ошибок является предпочтительным - между установкой onRejected или использованием catch. Ответ второй - улов. Проблема с onRejected в том, что если ошибка происходит в onResolved, onRejected просто обращает на нее внимание, тогда как catch ее поймает!

getJSON('baidu.com').then(data => {
  [].move();
}, (err) => {
  console.log('operation failed', err);  // turn blind eyes to error
});
delJSON('google.com').then(data => {
  [].move();
}).catch(err => {
  console.log('operation failed', err); // caught! since I see it
});

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

getJSON('baidu.com').then(data => {
  data.move();
}).catch(err => {
  console.log('1st error ', err);
}).then(data => {
  data.move();
}).catch(err => {
  console.log('2nd error ', err);
});
// prints both errors

Catch позволяет эффективно выявлять ошибки в этих местах.

  • Аргумент функции конструктора обещаний
  • onResolved
  • функции, возвращающие новый экземпляр обещания в onResolved

Однако указанная ниже ошибка не будет обнаружена, поскольку обещание уже разрешено, а состояние остается, и никакие операции на него не повлияют.

var promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('not ok');
});
promise
  .then(function(value) { console.log(value) }) // ok
  .catch(function(error) { console.log(error) }); // not called

Promise.resolve и Promise.reject

Мы также можем вызвать эти два метода в конструкторе Promise с примитивными значениями.

Promise.resolve('foo');
new Promise(resolve => resolve('foo'));
// above expressions are the same
Promise.reject('bar')
new Promise(reject => reject('bar'));

При выполнении будет создан экземпляр обещания с состоянием resolved или rejected.

Promise.all работает последовательно или параллельно?

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

const p = Promise.all([delJSON('google.com'), getJSON('baidu.com']);

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

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

  • Три состояния: pending, resolved, rejected
  • Вызов конструктора Promise с функцией в качестве единственного аргумента, который принимает resolve и reject два обратных вызова
  • Для успешной / неудачной операции вызовите resolve с data или reject с ошибкой, которая, в свою очередь, вызовет then или catch
  • Вложенное обещание определяет состояния внешнего обещания
  • Для лучшего покрытия ошибок используйте catch, а не onRejected
  • Вызов Promise.resolve или Promise.reject с примитивными значениями в основном генерирует новый экземпляр обещания
  • Promise.all заботится только о состоянии, а не о том, в каком порядке выполняются обещания
  • Используйте then().catch().then().catch() для обработки ошибок в различных операциях

Надеюсь, я помогу вам лучше понять Promise.

Далее я расскажу об использовании Async/Await в ES7.