Конструктор Promise()

Конструктор Promise() (проміс) створює об'єкти Promise. Він використовується в основному для обгортання API, які використовують зворотні виклики, але ще не підтримують проміси.

Спробуйте його в дії

Синтаксис

new Promise(executor)

Примітка: Promise() можна конструювати лише за допомогою new. Спроба викликати його без new викидає TypeError.

Параметри

executor

Функція, що викликається конструктором. Вона отримує як параметри дві функції: resolveFunc і rejectFunc. Будь-які помилки, що виникають в executor, призводять до відхилення проміса, а повернене значення ігнорується. Семантика executor детально описана нижче.

Повернене значення

Бувши викликаним за допомогою new, конструктор Promise повертає об'єкт проміса. Об'єкт проміса стає вирішеним, коли закликається або функція resolveFunc, або функція rejectFunc. Зверніть увагу, що якщо викликати resolveFunc або rejectFunc і передати інший об'єкт Promise як аргумент, то можна сказати, що проміс стає "вирішеним", але ще не "залагодженим". Дивіться опис Promise для отримання додаткових пояснень.

Опис

Традиційно (до промісів) асинхронні задачі оброблялися за допомогою зворотних викликів.

readFile("./data.txt", (error, result) => {
  // Ця функція зворотного виклику викликається, коли завдання виконано, з
  // результатом – `error` або `result`. Будь-яка операція, що залежить від
  // результату, повинна бути визначена в цій функції зворотного виклику.
});
// Код тут викликається зразу після того, як запит `readFile`
// запускається. Він не чекає виклику функції зворотного виклику, таким чином
// роблячи `readFile` "асинхронним".

Щоб скористатися перевагами поліпшення читабельності та можливостей мови, які пропонують проміси, конструктор Promise() дозволяє перетворити API на основі зворотних викликів на API на основі промісів.

Примітка: Якщо ваша задача вже заснована на промісах, вам, ймовірно, не потрібен конструктор Promise().

Функція executor – це клієнтський код, що зв'язує результат виклику функції зворотного виклику з промісом. Ви, програміст, пишете функцію executor. Очікується, що її сигнатура буде такою:

function executor(resolveFunc, rejectFunc) {
  // Зазвичай – якась асинхронна операція, що приймає функцію зворотного виклику,
  // подібна до функції `readFile` вище
}

Параметри resolveFunc і rejectFunc також є функціями, і їм можна дати будь-які фактичні імена. Їх сигнатури прості: вони приймають один параметр будь-якого типу.

resolveFunc(value); // виклик при вирішенні
rejectFunc(reason); // виклик при відхиленні

Параметр value, переданий до resolveFunc, може бути ще одним об'єктом-промісом, і в такому випадку стан новоствореного проміса буде "зав'язаний" на стан переданого проміса (як частина проміса вирішення). Функція rejectFunc має семантику, близьку до інструкції throw, тому reason зазвичай є примірником Error. Якщо value або reason відсутні, то проміс сповнюється або відхиляється з undefined.

Стан завершення executor має обмежений вплив на стан проміса:

  • Повернене з executor значення ігнорується. Інструкції return всередині executor впливають лише на потік керування і змінюють те, чи виконуються певні частини функції, але не мають жодного впливу на значення сповнення проміса. Якщо executor завершується і неможливо, щоб resolveFunc або rejectFunc були викликані в майбутньому (наприклад, немає запланованих асинхронних задач), то такий проміс залишається назавжди в стані очікування.
  • Якщо помилка викидається всередині executor, то проміс відхиляється, якщо resolveFunc і rejectFunc ще не були викликані.

Примітка: Існування промісів у стані очікування не заважає програмі завершитися. Якщо цикл подій порожній, програма завершується, не зважаючи на будь-які проміси у стані очікування (тому що вони обов'язково залишаються у стані очікування назавжди).

Ось нарис типового плину виконання:

  1. Коли конструктор породжує новий об'єкт Promise, він також породжує відповідну пару функцій resolveFunc і rejectFunc; вони "припнуті" до об'єкта Promise.
  2. Функція executor зазвичай обгортає якусь асинхронну операцію, що надає API на основі зворотного виклику. Функція зворотного виклику (та, що передається в оригінальний API на основі зворотних викликів) визначається всередині коду executor, тому вона має доступ до resolveFunc і rejectFunc.
  3. Функція executor викликається синхронно (як тільки Promise створено) з функціями resolveFunc і rejectFunc як аргументами.
  4. Код всередині executor має можливість виконати якусь операцію. Примірник проміса повідомляється про завершення асинхронної задачі за допомогою побічного ефекту, спричиненого resolveFunc або rejectFunc. Побічний ефект полягає в тому, що об'єкт Promise стає "вирішеним".
    • Якщо спершу викликається resolveFunc, то передане значення буде вирішенням. Проміс може залишитися у стані очікування (якщо передається інший очікуваний об'єкт), стати сповненим (у більшості випадків, коли передається не очікуване значення) або відхилитися (у випадку недійсного значення вирішення).
    • Якщо першою викликається rejectFunc, то проміс миттєво відхиляється.
    • Коли вже була викликана одна з функцій вирішення (resolveFunc або rejectFunc), проміс залишається вирішеним. Тільки перший виклик resolveFunc або rejectFunc впливає на кінцевий стан проміса, і наступні виклики жодним чином не можуть ані змінити значення сповнення чи причину відхилення, ані перемкнути його кінцевий стан зі "сповненого" на "відхилений" або навпаки.
    • Якщо executor завершується викиданням помилки, то проміс відхиляється. Однак помилка ігнорується, якщо одна з функцій вирішення вже була викликана (таким чином, проміс вже вирішено).
    • Вирішення проміса не обов'язково змушує проміс стати сповненим або відхиленим (тобто залагодженим). Цей проміс може залишитися в стані очікування, якщо його вирішено іншим очікуваним об'єктом, але його кінцевий стан буде відповідати кінцевому стану цього вирішеного очікуваного об'єкта.
  5. Коли проміс залагоджено, він (асинхронно) закликає всі подальші обробники, прив'язані за допомогою then(), catch() або finally(). Значення сповнення або причина відхилення передаються виклику обробників сповнення та відхилення як вхідний параметр (дивіться Ланцюжки промісів).

Наприклад, API readFile на основі зворотних викликів вище можна перетворити на API на основі промісів.

const readFilePromise = (path) =>
  new Promise((resolve, reject) => {
    readFile(path, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });

readFilePromise("./data.txt")
  .then((result) => console.log(result))
  .catch((error) => console.error("Не вийшло прочитати дані"));

Функції зворотного виклику resolve і reject доступні лише всередині області видимості функції executor, тобто не можна звернутися до них після конструювання проміса. Якщо потрібно сконструювати проміс до прийняття рішення щодо того, як його вирішувати, можна натомість скористатися методом Promise.withResolvers, який видає функції resolve і reject.

Функція resolve

Функція resolve має наступну логіку:

  • Якщо вона викликана з таким же значенням, як і новостворений проміс (проміс, до якого вона "припнута"), то цей проміс відхиляється з помилкою TypeError.
  • Якщо вона викликається з не очікуваним значенням (примітивом, або об'єктом, чию властивість then не можна викликати, в тому числі коли вона відсутня), то її проміс негайно сповнюється цим значенням.
  • Якщо вона викликається з очікуваним значенням (наприклад, іншим примірником Promise), то зберігається метод then цього значення – він буде викликаний у майбутньому (і завжди асинхронно). Цей метод then буде викликаний з двома функціями зворотного виклику, які будуть двома новими функціями з такою ж логікою, як у функцій resolveFunc і rejectFunc, переданих у функцію executor. Якщо виклик цього методу then викидає помилку, то поточний проміс відхиляється з викинутою помилкою.

В останньому випадку, це означає, що подібний код:

new Promise((resolve, reject) => {
  resolve(thenable);
});

Приблизно рівносильний такому:

new Promise((resolve, reject) => {
  try {
    thenable.then(
      (value) => resolve(value),
      (reason) => reject(reason),
    );
  } catch (e) {
    reject(e);
  }
});

Окрім того, що в випадку resolve(thenable):

  1. Функція resolve викликається синхронно, тож повторний виклик resolve або reject ніяк не діє, навіть коли обробники, приєднані за допомогою anotherPromise.then(), ще не викликані.
  2. Метод then викликається асинхронно, тож проміс ніколи не буде миттєво сповненим, якщо передається очікуваний об'єкт.

У зв'язку з тим, що resolve викликається знову з тим, що thenable.then() передає йому як value, функція вирішення може сплющувати вкладені очікувані об'єкти, де очікуваний об'єкт викликає свій обробник onFulfilled з іншим очікуваним об'єктом. Це означає, що обробник сповнення справжнього проміса ніколи не отримає очікуваний об'єкт як значення сповнення.

Приклади

Перетворення API на основі зворотних викликів на API на основі промісів

Щоб надати функції функціональність промісів, вона повинна повертати проміс, викликаючи функції resolve і reject у відповідний час.

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
}

Вплив виклику resolveFunc

Виклик resolveFunc змушує проміс стати вирішеним, тож подальші виклики resolveFunc або rejectFunc не мають жодного впливу. Однак проміс може перебувати в будь-якому стані: очікування, сповненості або відхиленості

Цей проміс pendingResolved вирішений в момент створення, тому що він вже "замкнений" на відповідність остаточному станові внутрішнього проміса, і подальший виклик resolveOuter або rejectOuter або викидання помилки пізніше в функції-виконавці не має жодного впливу на його остаточний стан. Однак внутрішній проміс все ж перебуває в стані очікування, поки не мине 100 мс, тож зовнішній проміс також перебуває у стані очікування:

const pendingResolved = new Promise((resolveOuter, rejectOuter) => {
  resolveOuter(
    new Promise((resolveInner) => {
      setTimeout(() => {
        resolveInner("внутрішній");
      }, 100);
    }),
  );
});

Цей проміс fulfilledResolved стає сповненим в момент вирішення, тому що він вирішується значенням, яке не є очікуваним об'єктом. Однак коли він створюється, він є невирішеним, тому що ані resolve, ані reject ще не були викликані. Невирішений проміс обов'язково перебуває в стані очікування:

const fulfilledResolved = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("зовнішній");
  }, 100);
});

Виклик rejectFunc очевидно призводить до відхилення проміса. Однак є ще два способи, якими можна змусити проміс миттєво відхилитися, навіть коли викликається функція resolveFunc.

// 1. Вирішення проміса самим собою
const rejectedResolved1 = new Promise((resolve) => {
  // Примітка: resolve має бути викликана асинхронно,
  // щоб ініціалізувалася змінна rejectedResolved1
  setTimeout(() => resolve(rejectedResolved1)); // TypeError: Chaining cycle detected for promise #<Promise>
});

// 2. Вирішення об'єктом, що викидає помилку, коли відбувається звертання до властивості `then`
const rejectedResolved2 = new Promise((resolve) => {
  resolve({
    get then() {
      throw new Error("Не можна отримати властивість then");
    },
  });
});

Специфікації

Сумісність із браузерами

desktop mobile server
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet Deno Node.js
Promise() constructor
Chrome Full support 32
Edge Full support 12
Firefox Full support 29
footnote
Internet Explorer No support No
Opera Full support 19
Safari Full support 8
footnote
WebView Android Full support 4.4.3
Chrome Android Full support 32
Firefox for Android Full support 29
footnote
Opera Android Full support 19
Safari on iOS Full support 8
footnote
Samsung Internet Full support 2.0
Deno Full support 1.0
Node.js Full support 0.12.0
footnote

Дивіться також