JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

40 Promises für asynchrone Programmierung [ES6]



  Empfohlene Lektüre

Dieses Kapitel baut auf dem vorherigen Kapitel mit Hintergrundinformationen zur asynchronen Programmierung in JavaScript auf.

40.1 Die Grundlagen der Verwendung von Promises

Promises sind eine Technik zur asynchronen Lieferung von Ergebnissen.

40.1.1 Verwendung einer Promise-basierten Funktion

Der folgende Code ist ein Beispiel für die Verwendung der Promise-basierten Funktion addAsync() (deren Implementierung bald gezeigt wird)

addAsync(3, 4)
  .then(result => { // success
    assert.equal(result, 7);
  })
  .catch(error => { // failure
    assert.fail(error);
  });

Promises ähneln dem Event-Muster: Es gibt ein Objekt (ein Promise), bei dem wir Callbacks registrieren

Eine Promise-basierte Funktion gibt ein Promise zurück und sendet ihm ein Ergebnis oder einen Fehler (sobald sie fertig ist). Das Promise leitet es an die entsprechenden Callbacks weiter.

Im Gegensatz zum Event-Muster sind Promises für einmalige Ergebnisse optimiert

40.1.2 Was ist ein Promise?

Was ist ein Promise? Es gibt zwei Sichtweisen darauf

40.1.3 Implementierung einer Promise-basierten Funktion

Dies ist eine Implementierung einer Promise-basierten Funktion, die zwei Zahlen x und y addiert

function addAsync(x, y) {
  return new Promise(
    (resolve, reject) => { // (A)
      if (x === undefined || y === undefined) {
        reject(new Error('Must provide two parameters'));
      } else {
        resolve(x + y);
      }
    });
}

addAsync() ruft sofort den Promise-Konstruktor auf. Die eigentliche Implementierung dieser Funktion befindet sich im Callback, der diesem Konstruktor übergeben wird (Zeile A). Dieser Callback erhält zwei Funktionen

40.1.4 Zustände von Promises

Figure 22: A Promise can be in either one of three states: pending, fulfilled, or rejected. If a Promise is in a final (non-pending) state, it is called settled.

Abb. 22 zeigt die drei Zustände, in denen sich ein Promise befinden kann. Promises sind auf einmalige Ergebnisse spezialisiert und schützen uns vor Race Conditions (zu frühes oder zu spätes Registrieren)

Darüber hinaus kann sich der Zustand und der Abschlusswert eines Promises nach dem Abschluss nicht mehr ändern. Das hilft, Code vorhersehbar zu machen und die einmalige Natur von Promises zu erzwingen.

  Manche Promises werden nie abgeschlossen

Es ist möglich, dass ein Promise nie abgeschlossen wird. Zum Beispiel

new Promise(() => {})

40.1.5 Promise.resolve(): Erstellen eines Promises, das mit einem gegebenen Wert erfüllt wird

Promise.resolve(x) erstellt ein Promise, das mit dem Wert x erfüllt wird.

Promise.resolve(123)
  .then(x => {
    assert.equal(x, 123);
  });

Wenn der Parameter bereits ein Promise ist, wird es unverändert zurückgegeben.

const abcPromise = Promise.resolve('abc');
assert.equal(
  Promise.resolve(abcPromise),
  abcPromise);

Daher können wir mit Promise.resolve(x) sicherstellen, dass wir ein Promise haben, gegebenenfalls einen beliebigen Wert x.

Beachten Sie, dass der Name resolve und nicht fulfill lautet, da .resolve() ein abgelehntes Promise zurückgibt, wenn sein Parameter ein abgelehntes Promise ist.

40.1.6 Promise.reject(): Erstellen eines Promises, das mit einem gegebenen Wert abgelehnt wird

Promise.reject(err) erstellt ein Promise, das mit dem Wert err abgelehnt wird.

const myError = new Error('My error!');
Promise.reject(myError)
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.7 Rückgabe und Auslösen in .then()-Callbacks

.then() behandelt Promise-Erfüllungen. Es gibt auch ein neues Promise zurück. Wie dieses Promise abgeglichen wird, hängt davon ab, was im Callback passiert. Betrachten wir drei gängige Fälle.

40.1.7.1 Rückgabe eines Nicht-Promise-Wertes

Erstens kann der Callback einen Nicht-Promise-Wert zurückgeben (Zeile A). Folglich wird das von .then() zurückgegebene Promise mit diesem Wert erfüllt (wie in Zeile B geprüft)

Promise.resolve('abc')
  .then(str => {
    return str + str; // (A)
  })
  .then(str2 => {
    assert.equal(str2, 'abcabc'); // (B)
  });
40.1.7.2 Rückgabe eines Promises

Zweitens kann der Callback ein Promise p zurückgeben (Zeile A). Folglich “wird” p zu dem, was .then() zurückgibt. Mit anderen Worten: Das von .then() bereits zurückgegebene Promise wird effektiv durch p ersetzt.

Promise.resolve('abc')
  .then(str => {
    return Promise.resolve(123); // (A)
  })
  .then(num => {
    assert.equal(num, 123);
  });

Warum ist das nützlich? Wir können das Ergebnis einer Promise-basierten Operation zurückgeben und seinen Erfüllungswert über ein „flaches“ (nicht verschachteltes) .then() verarbeiten. Vergleichen Sie

// Flat
asyncFunc1()
  .then(result1 => {
    /*···*/
    return asyncFunc2();
  })
  .then(result2 => {
    /*···*/
  });

// Nested
asyncFunc1()
  .then(result1 => {
    /*···*/
    asyncFunc2()
    .then(result2 => {
      /*···*/
    });
  });
40.1.7.3 Auslösen einer Ausnahme

Drittens kann der Callback eine Ausnahme auslösen. Folglich wird das von .then() zurückgegebene Promise mit dieser Ausnahme abgelehnt. Das heißt, ein synchroner Fehler wird in einen asynchronen Fehler umgewandelt.

const myError = new Error('My error!');
Promise.resolve('abc')
  .then(str => {
    throw myError;
  })
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.8 .catch() und sein Callback

Der Unterschied zwischen .then() und .catch() besteht darin, dass letzteres durch Ablehnungen und nicht durch Erfüllungen ausgelöst wird. Beide Methoden wandeln die Aktionen ihrer Callbacks jedoch auf die gleiche Weise in Promises um. Zum Beispiel wird in der folgenden Code der von der .catch()-Callback in Zeile A zurückgegebene Wert ein Erfüllungswert

const err = new Error();

Promise.reject(err)
  .catch(e => {
    assert.equal(e, err);
    // Something went wrong, use a default value
    return 'default value'; // (A)
  })
  .then(str => {
    assert.equal(str, 'default value');
  });

40.1.9 Ketten von Methodenaufrufen

.then() und .catch() geben immer Promises zurück. Das ermöglicht es uns, beliebig lange Ketten von Methodenaufrufen zu erstellen

function myAsyncFunc() {
  return asyncFunc1() // (A)
    .then(result1 => {
      // ···
      return asyncFunc2(); // a Promise
    })
    .then(result2 => {
      // ···
      return result2 ?? '(Empty)'; // not a Promise
    })
    .then(result3 => {
      // ···
      return asyncFunc4(); // a Promise
    });
}

Aufgrund der Verkettung gibt das return in Zeile A das Ergebnis des letzten .then() zurück.

In gewisser Weise ist .then() die asynchrone Version des synchronen Semikolons

Wir können auch .catch() in die Mischung einbeziehen und es mehrere Fehlerquellen gleichzeitig verarbeiten lassen

asyncFunc1()
  .then(result1 => {
    // ···
    return asyncFunction2();
  })
  .then(result2 => {
    // ···
  })
  .catch(error => {
    // Failure: handle errors of asyncFunc1(), asyncFunc2()
    // and any (sync) exceptions thrown in previous callbacks
  });

40.1.10 .finally() [ES2018]

Die Promise-Methode .finally() wird oft wie folgt verwendet

somePromise
  .then((result) => {
    // ···
  })
  .catch((error) => {
    // ···
  })
  .finally(() => {
    // ···
  })
;

Der .finally()-Callback wird immer ausgeführt – unabhängig von somePromise und den von .then() und/oder .catch() zurückgegebenen Werten. Im Gegensatz dazu

.finally() ignoriert, was sein Callback zurückgibt, und gibt einfach die vorherige Abgleichung weiter

Promise.resolve(123)
  .finally(() => {})
  .then((result) => {
    assert.equal(result, 123);
  });

Promise.reject('error')
  .finally(() => {})
  .catch((error) => {
    assert.equal(error, 'error');
  });

Wenn jedoch der .finally()-Callback eine Ausnahme auslöst, wird das von .finally() zurückgegebene Promise abgelehnt.

Promise.reject('error (originally)')
  .finally(() => {
    throw 'error (finally)';
  })
  .catch((error) => {
    assert.equal(error, 'error (finally)');
  });
40.1.10.1 Anwendungsfall für .finally(): Bereinigung

Ein häufiger Anwendungsfall für .finally() ähnelt einem häufigen Anwendungsfall der synchronen finally-Klausel: die Bereinigung, nachdem Sie mit einer Ressource fertig sind. Dies sollte immer geschehen, unabhängig davon, ob alles reibungslos verlief oder ein Fehler aufgetreten ist – zum Beispiel

let connection;
db.open()
.then((conn) => {
  connection = conn;
  return connection.select({ name: 'Jane' });
})
.then((result) => {
  // Process result
  // Use `connection` to make more queries
})
// ···
.catch((error) => {
  // handle errors
})
.finally(() => {
  connection.close();
});
40.1.10.2 Anwendungsfall für .finally(): Etwas zuerst nach jeder Art von Abgleichung tun

Wir können .finally() auch vor sowohl .then() als auch .catch() verwenden. Dann wird das, was wir im .finally()-Callback tun, immer vor den anderen beiden Callbacks ausgeführt.

Beispielsweise geschieht dies mit einem erfüllten Promise

Promise.resolve('fulfilled')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'then fulfilled'

Dies geschieht mit einem abgelehnten Promise

Promise.reject('rejected')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'catch rejected'

40.1.11 Vorteile von Promises gegenüber einfachen Callbacks

Dies sind einige der Vorteile von Promises gegenüber einfachen Callbacks bei der Verarbeitung von einmaligen Ergebnissen

Einer der größten Vorteile von Promises liegt darin, dass man nicht direkt mit ihnen arbeitet: Sie sind die Grundlage von Async-Funktionen, einer synchron aussehenden Syntax zur Durchführung asynchroner Berechnungen. Asynchrone Funktionen werden im nächsten Kapitel behandelt.

40.2 Beispiele

Promises in Aktion zu sehen hilft beim Verstehen. Werfen wir einen Blick auf Beispiele.

40.2.1 Node.js: Eine Datei asynchron lesen

Betrachten Sie die folgende Textdatei person.json mit JSON-Daten darin

{
  "first": "Jane",
  "last": "Doe"
}

Werfen wir einen Blick auf zwei Versionen von Code, die diese Datei lesen und in ein Objekt parsen. Erstens, eine Callback-basierte Version. Zweitens, eine Promise-basierte Version.

40.2.1.1 Die Callback-basierte Version

Der folgende Code liest den Inhalt dieser Datei und wandelt ihn in ein JavaScript-Objekt um. Er basiert auf Node.js-typischen Callbacks

import * as fs from 'fs';
fs.readFile('person.json',
  (error, text) => {
    if (error) { // (A)
      // Failure
      assert.fail(error);
    } else {
      // Success
      try { // (B)
        const obj = JSON.parse(text); // (C)
        assert.deepEqual(obj, {
          first: 'Jane',
          last: 'Doe',
        });
      } catch (e) {
        // Invalid JSON
        assert.fail(e);
      }
    }
  });

fs ist ein integriertes Node.js-Modul für Dateisystemoperationen. Wir verwenden die Callback-basierte Funktion fs.readFile(), um eine Datei namens person.json zu lesen. Wenn wir erfolgreich sind, wird der Inhalt über den Parameter text als String geliefert. In Zeile C wandeln wir diesen String vom textbasierten Datenformat JSON in ein JavaScript-Objekt um. JSON ist ein Objekt mit Methoden zum Konsumieren und Produzieren von JSON. Es ist Teil der Standardbibliothek von JavaScript und wird später in diesem Buch dokumentiert.

Beachten Sie, dass es zwei Fehlerbehandlungsmechanismen gibt: Das if in Zeile A kümmert sich um asynchrone Fehler, die von fs.readFile() gemeldet werden, während das try in Zeile B sich um synchrone Fehler kümmert, die von JSON.parse() gemeldet werden.

40.2.1.2 Die Promise-basierte Version

Der folgende Code verwendet readFileAsync(), eine Promise-basierte Version von fs.readFile() (erstellt über util.promisify(), das später erklärt wird)

readFileAsync('person.json')
  .then(text => { // (A)
    // Success
    const obj = JSON.parse(text);
    assert.deepEqual(obj, {
      first: 'Jane',
      last: 'Doe',
    });
  })
  .catch(err => { // (B)
    // Failure: file I/O error or JSON syntax error
    assert.fail(err);
  });

Die Funktion readFileAsync() gibt ein Promise zurück. In Zeile A geben wir einen Erfolgs-Callback über die Methode .then() dieses Promises an. Der restliche Code im then-Callback ist synchron.

.then() gibt ein Promise zurück, das die Ausführung der Promise-Methode .catch() in Zeile B ermöglicht. Wir verwenden es, um einen Fehler-Callback anzugeben.

Beachten Sie, dass .catch() uns ermöglicht, sowohl die asynchronen Fehler von readFileAsync() als auch die synchronen Fehler von JSON.parse() zu behandeln, da Ausnahmen innerhalb eines .then()-Callbacks zu Ablehnungen werden.

40.2.2 Browser: XMLHttpRequest promisfizieren

Wir haben zuvor die ereignisbasierte XMLHttpRequest-API zum Herunterladen von Daten in Webbrowsern gesehen. Die folgende Funktion promisfiziert diese API

function httpGet(url) {
  return new Promise(
    (resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(xhr.responseText); // (A)
        } else {
          // Something went wrong (404, etc.)
          reject(new Error(xhr.statusText)); // (B)
        }
      }
      xhr.onerror = () => {
        reject(new Error('Network error')); // (C)
      };
      xhr.open('GET', url);
      xhr.send();
    });
}

Beachten Sie, wie die Ergebnisse und Fehler von XMLHttpRequest über resolve() und reject() behandelt werden.

So verwenden Sie httpGet()

httpGet('http://example.com/textfile.txt')
  .then(content => {
    assert.equal(content, 'Content of textfile.txt\n');
  })
  .catch(error => {
    assert.fail(error);
  });

  Übung: Timeout für ein Promise

exercises/promises/promise_timeout_test.mjs

40.2.3 Node.js: util.promisify()

util.promisify() ist eine Hilfsfunktion, die eine Callback-basierte Funktion f in eine Promise-basierte Funktion umwandelt. Das heißt, wir gehen von dieser Typsignatur aus

f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void

Zu dieser Typsignatur

f(arg_1, ···, arg_n) : Promise<T>

Der folgende Code promisfiziert die Callback-basierte Funktion fs.readFile() (Zeile A) und verwendet sie.

import * as fs from 'fs';
import {promisify} from 'util';

const readFileAsync = promisify(fs.readFile); // (A)

readFileAsync('some-file.txt', {encoding: 'utf8'})
  .then(text => {
    assert.equal(text, 'The content of some-file.txt\n');
  })
  .catch(err => {
    assert.fail(err);
  });

  Übungen: util.promisify()

40.2.4 Browser: Fetch API

Alle modernen Browser unterstützen Fetch, eine neue Promise-basierte API zum Herunterladen von Daten. Betrachten Sie sie als eine Promise-basierte Version von XMLHttpRequest. Das Folgende ist ein Auszug aus der API

interface Body {
  text() : Promise<string>;
  ···
}
interface Response extends Body {
  ···
}
declare function fetch(str) : Promise<Response>;

Das bedeutet, dass wir fetch() wie folgt verwenden können

fetch('http://example.com/textfile.txt')
  .then(response => response.text())
  .then(text => {
    assert.equal(text, 'Content of textfile.txt\n');
  });

  Übung: Verwendung der Fetch API

exercises/promises/fetch_json_test.mjs

40.3 Fehlerbehandlung: Ablehnungen und Ausnahmen nicht vermischen

Regel für die Implementierung von Funktionen und Methoden

Vermischen Sie keine (asynchronen) Ablehnungen und (synchronen) Ausnahmen.

Dies macht unseren synchronen und asynchronen Code vorhersehbarer und einfacher, da wir uns immer auf einen einzigen Fehlerbehandlungsmechanismus konzentrieren können.

Für Promise-basierte Funktionen und Methoden bedeutet die Regel, dass sie niemals Ausnahmen auslösen sollten. Leider ist es leicht, dies versehentlich falsch zu machen – zum Beispiel

// Don’t do this
function asyncFunc() {
  doSomethingSync(); // (A)
  return doSomethingAsync()
    .then(result => {
      // ···
    });
}

Das Problem ist, dass wenn in Zeile A eine Ausnahme ausgelöst wird, asyncFunc() eine Ausnahme auslöst. Aufrufer dieser Funktion erwarten nur Ablehnungen und sind nicht auf eine Ausnahme vorbereitet. Es gibt drei Möglichkeiten, dieses Problem zu beheben.

Wir können den gesamten Körper der Funktion in eine try-catch-Anweisung einpacken und ein abgelehntes Promise zurückgeben, wenn eine Ausnahme ausgelöst wird

// Solution 1
function asyncFunc() {
  try {
    doSomethingSync();
    return doSomethingAsync()
      .then(result => {
        // ···
      });
  } catch (err) {
    return Promise.reject(err);
  }
}

Da .then() Ausnahmen in Ablehnungen umwandelt, können wir doSomethingSync() innerhalb eines .then()-Callbacks ausführen. Dazu starten wir eine Promise-Kette über Promise.resolve(). Wir ignorieren den Erfüllungswert undefined dieses anfänglichen Promises.

// Solution 2
function asyncFunc() {
  return Promise.resolve()
    .then(() => {
      doSomethingSync();
      return doSomethingAsync();
    })
    .then(result => {
      // ···
    });
}

Schließlich wandelt auch new Promise() Ausnahmen in Ablehnungen um. Die Verwendung dieses Konstruktors ähnelt daher der vorherigen Lösung

// Solution 3
function asyncFunc() {
  return new Promise((resolve, reject) => {
      doSomethingSync();
      resolve(doSomethingAsync());
    })
    .then(result => {
      // ···
    });
}

40.4 Promise-basierte Funktionen starten synchron, werden asynchron abgeglichen

Die meisten Promise-basierten Funktionen werden wie folgt ausgeführt

Der folgende Code demonstriert dies

function asyncFunc() {
  console.log('asyncFunc');
  return new Promise(
    (resolve, _reject) => {
      console.log('new Promise()');
      resolve();
    });
}
console.log('START');
asyncFunc()
  .then(() => {
    console.log('.then()'); // (A)
  });
console.log('END');

// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'

Wir sehen, dass der Callback von new Promise() vor dem Ende des Codes ausgeführt wird, während das Ergebnis später geliefert wird (Zeile A).

Vorteile dieses Ansatzes

  Weitere Informationen zu diesem Ansatz

„Designing APIs for Asynchrony“ von Isaac Z. Schlueter

40.5 Promise-Kombinatorfunktionen: Arbeiten mit Promise-Arrays

40.5.1 Was ist eine Promise-Kombinatorfunktion?

Das Kombinator-Muster ist ein Muster in der funktionalen Programmierung zum Erstellen von Strukturen. Es basiert auf zwei Arten von Funktionen

Wenn es um JavaScript Promises geht

Als Nächstes werden wir die genannten Promise-Kombinatoren genauer betrachten.

40.5.2 Promise.all()

Dies ist die Typsignatur von Promise.all()

Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>

Promise.all() gibt ein Promise zurück, das

Dies ist eine schnelle Demo des Erfüllungs-Promises, das erfüllt wird

const promises = [
  Promise.resolve('result a'),
  Promise.resolve('result b'),
  Promise.resolve('result c'),
];
Promise.all(promises)
  .then((arr) => assert.deepEqual(
    arr, ['result a', 'result b', 'result c']
  ));

Das folgende Beispiel zeigt, was passiert, wenn mindestens eines der Eingabe-Promises abgelehnt wird

const promises = [
  Promise.resolve('result a'),
  Promise.resolve('result b'),
  Promise.reject('ERROR'),
];
Promise.all(promises)
  .catch((err) => assert.equal(
    err, 'ERROR'
  ));

Abb. 23 veranschaulicht, wie Promise.all() funktioniert.

Figure 23: The Promise combinator Promise.all().
40.5.2.1 Asynchrones .map() über Promise.all()

Array-Transformationsmethoden wie .map(), .filter() usw. sind für synchrone Berechnungen gedacht. Zum Beispiel

function timesTwoSync(x) {
  return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);

Was passiert, wenn der Callback von .map() eine Promise-basierte Funktion ist (eine Funktion, die normale Werte auf Promises abbildet)? Dann ist das Ergebnis von .map() ein Array von Promises. Leider sind das keine Daten, mit denen normaler Code arbeiten kann. Glücklicherweise können wir das mit Promise.all() beheben: Es konvertiert ein Array von Promises in ein Promise, das mit einem Array normaler Werte erfüllt wird.

function timesTwoAsync(x) {
  return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
  .then(result => {
    assert.deepEqual(result, [2, 4, 6]);
  });
40.5.2.2 Ein realistischeres .map()-Beispiel

Als Nächstes verwenden wir .map() und Promise.all(), um Textdateien aus dem Web herunterzuladen. Dafür benötigen wir die folgende Hilfsfunktion

function downloadText(url) {
  return fetch(url)
    .then((response) => { // (A)
      if (!response.ok) { // (B)
        throw new Error(response.statusText);
      }
      return response.text(); // (C)
    });
}

downloadText() verwendet die Promise-basierte Fetch-API, um eine Textdatei als String herunterzuladen.

Im folgenden Beispiel laden wir zwei Textdateien herunter

const urls = [
  'http://example.com/first.txt',
  'http://example.com/second.txt',
];

const promises = urls.map(
  url => downloadText(url));

Promise.all(promises)
  .then(
    (arr) => assert.deepEqual(
      arr, ['First!', 'Second!']
    ));
40.5.2.3 Eine einfache Implementierung von Promise.all()

Dies ist eine vereinfachte Implementierung von Promise.all() (z. B. führt sie keine Sicherheitsprüfungen durch)

function all(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    let index = 0;
    for (const promise of iterable) {
      // Preserve the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => {
          result[currentIndex] = value;
          elementCount++;
          if (elementCount === result.length) {
            resolve(result); // (A)
          }
        },
        (err) => {
          reject(err); // (B)
        });
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

Die beiden Hauptstellen, an denen das Ergebnis-Promise abgeglichen wird, sind Zeile A und Zeile B. Nachdem eine davon abgeglichen wurde, kann die andere den Abgleichungswert nicht mehr ändern, da ein Promise nur einmal abgeglichen werden kann.

40.5.3 Promise.race()

Dies ist die Typsignatur von Promise.race()

Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>

Promise.race() gibt ein Promise q zurück, das abgeglichen wird, sobald das erste Promise p unter promises abgeglichen wird. q hat denselben Abgleichungswert wie p.

In der folgenden Demo geschieht die Abgleichung des erfüllten Promises (Zeile A) vor der Abgleichung des abgelehnten Promises (Zeile B). Daher ist das Ergebnis auch erfüllt (Zeile C).

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve('result'), 100)), // (A)
  new Promise((resolve, reject) =>
    setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
  .then((result) => assert.equal( // (C)
    result, 'result'));

In der nächsten Demo geschieht die Ablehnung zuerst

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve('result'), 200)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
  .then(
    (result) => assert.fail(),
    (err) => assert.equal(
      err, 'ERROR'));

Beachten Sie, dass das von Promise.race() zurückgegebene Promise so bald wie das erste seiner Eingabe-Promises abgeglichen wird, abgeglichen wird. Das bedeutet, dass das Ergebnis von Promise.race([]) nie abgeglichen wird.

Abb. 24 veranschaulicht, wie Promise.race() funktioniert.

Figure 24: The Promise combinator Promise.race().
40.5.3.1 Verwendung von Promise.race() zum Timeout eines Promises

In diesem Abschnitt verwenden wir Promise.race(), um Promises mit einem Timeout zu versehen. Die folgende Hilfsfunktion wird mehrmals nützlich sein

function resolveAfter(ms, value=undefined) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), ms);
  });
}

resolveAfter() gibt ein Promise zurück, das nach ms Millisekunden mit value aufgelöst wird.

Diese Funktion macht ein Promise zu einem Timeout

function timeout(timeoutInMs, promise) {
  return Promise.race([
    promise,
    resolveAfter(timeoutInMs,
      Promise.reject(new Error('Operation timed out'))),
  ]);
}

timeout() gibt ein Promise zurück, dessen Abgleichung dieselbe ist wie die, welches der beiden Promises zuerst abgeglichen wird

  1. Der Parameter promise
  2. Ein Promise, das nach timeoutInMs Millisekunden abgelehnt wird.

Um das zweite Promise zu erzeugen, nutzt timeout() die Tatsache, dass das Auflösen eines ausstehenden Promises mit einem abgelehnten Promise dazu führt, dass das erstere abgelehnt wird.

Lassen Sie uns timeout() in Aktion sehen. Hier wird das Eingabe-Promise vor dem Timeout erfüllt. Daher wird das Ausgabe-Promise erfüllt.

timeout(200, resolveAfter(100, 'Result!'))
  .then(result => assert.equal(result, 'Result!'));

Hier geschieht das Timeout, bevor das Eingabe-Promise erfüllt wird. Daher wird das Ausgabe-Promise abgelehnt.

timeout(100, resolveAfter(2000, 'Result!'))
  .catch(err => assert.deepEqual(err, new Error('Operation timed out')));

Es ist wichtig zu verstehen, was „Timeout eines Promises“ wirklich bedeutet

Das heißt, das Timeout verhindert nur, dass das Eingabe-Promise das Ausgabe-Promise beeinflusst (da ein Promise nur einmal abgeglichen werden kann). Aber es stoppt nicht die asynchrone Operation, die das Eingabe-Promise erzeugt hat.

40.5.3.2 Eine einfache Implementierung von Promise.race()

Dies ist eine vereinfachte Implementierung von Promise.race() (z. B. führt sie keine Sicherheitsprüfungen durch)

function race(iterable) {
  return new Promise((resolve, reject) => {
    for (const promise of iterable) {
      promise.then(
        (value) => {
          resolve(value); // (A)
        },
        (err) => {
          reject(err); // (B)
        });
    }
  });
}

Das Ergebnis-Promise wird entweder in Zeile A oder in Zeile B abgeglichen. Sobald dies geschehen ist, kann der Abgleichungswert nicht mehr geändert werden.

40.5.4 Promise.any() und AggregateError [ES2021]

Dies ist die Typsignatur von Promise.any()

Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>

Promise.any() gibt ein Promise p zurück. Wie es abgeglichen wird, hängt vom Parameter promises ab (der sich auf ein Iterable von Promises bezieht)

Dies ist die Typsignatur von AggregateError (eine Unterklasse von Error)

class AggregateError extends Error {
  // Instance properties (complementing the ones of Error)
  errors: Array<any>;

  constructor(
    errors: Iterable<any>,
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

Abb. 25 veranschaulicht, wie Promise.any() funktioniert.

Figure 25: The Promise combinator Promise.any().
40.5.4.1 Zwei erste Beispiele

Dies ist, was passiert, wenn ein Promise erfüllt wird

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
];
Promise.any(promises)
  .then((result) => assert.equal(
    result, 'result'
  ));

Dies ist, was passiert, wenn alle Promises abgelehnt werden

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
];
Promise.any(promises)
  .catch((aggregateError) => assert.deepEqual(
    aggregateError.errors,
    ['ERROR A', 'ERROR B', 'ERROR C']
  ));
40.5.4.2 Promise.any() vs. Promise.all()

Es gibt zwei Möglichkeiten, wie Promise.any() und Promise.all() verglichen werden können

40.5.4.3 Promise.any() vs. Promise.race()

Promise.any() und Promise.race() sind ebenfalls verwandt, interessieren sich aber für unterschiedliche Dinge

Der Haupt – relativ seltene – Anwendungsfall für .race() ist das Timeout von Promises. Die Anwendungsfälle für .any() sind breiter. Wir werden sie als Nächstes betrachten.

40.5.4.4 Anwendungsfälle für Promise.any()

Wir verwenden Promise.any(), wenn wir mehrere asynchrone Berechnungen haben und uns nur die erste erfolgreiche interessiert. In gewisser Weise lassen wir die Berechnungen miteinander konkurrieren und verwenden, welche auch immer am schnellsten ist.

Der folgende Code demonstriert, wie das beim Herunterladen von Ressourcen aussieht

const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

Das gleiche Muster ermöglicht es uns, welches Modul auch immer schneller herunterlädt

const lodash = await Promise.any([
  import('https://primary.example.com/lodash'),
  import('https://secondary.example.com/lodash'),
]);

Zum Vergleich, dies ist der Code, den wir verwenden würden, wenn der sekundäre Server nur ein Fallback ist – falls der primäre Server fehlschlägt.

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
40.5.4.5 Wie würden wir Promise.any() implementieren?

Eine einfache Implementierung von Promise.any() ist im Grunde eine Spiegelversion der Implementierung von Promise.all().

40.5.5 Promise.allSettled() [ES2020]

Diesmal sind die Typsignaturen etwas komplizierter. Überspringen Sie ruhig zur ersten Demo, die leichter zu verstehen sein sollte.

Dies ist die Typsignatur von Promise.allSettled()

Promise.allSettled<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<SettlementObject<T>>>

Es gibt ein Promise für ein Array zurück, dessen Elemente die folgende Typsignatur haben

type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;

interface FulfillmentObject<T> {
  status: 'fulfilled';
  value: T;
}

interface RejectionObject {
  status: 'rejected';
  reason: unknown;
}

Promise.allSettled() gibt ein Promise out zurück. Sobald alle promises abgeglichen sind, wird out mit einem Array erfüllt. Jedes Element e dieses Arrays entspricht einem Promise p von promises

Sofern beim Iterieren über promises kein Fehler auftritt, wird das Ausgabe-Promise out niemals abgelehnt.

Abb. 26 veranschaulicht, wie Promise.allSettled() funktioniert.

Figure 26: The Promise combinator Promise.allSettled().
40.5.5.1 Eine erste Demo von Promise.allSettled()

Dies ist eine kurze erste Demo, wie Promise.allSettled() funktioniert

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
  { status: 'fulfilled', value:  'a' },
  { status: 'rejected',  reason: 'b' },
]));
40.5.5.2 Ein längeres Beispiel für Promise.allSettled()

Das nächste Beispiel ähnelt dem Beispiel ".map()" plus "Promise.all()" (von dem wir die Funktion downloadText() übernehmen): Wir laden mehrere Textdateien herunter, deren URLs in einem Array gespeichert sind. Diesmal möchten wir jedoch nicht bei einem Fehler aufhören, sondern weitermachen. Promise.allSettled() ermöglicht uns das

const urls = [
  'http://example.com/exists.txt',
  'http://example.com/missing.txt',
];

const result = Promise.allSettled(
  urls.map(u => downloadText(u)));
result.then(
  arr => assert.deepEqual(
    arr,
    [
      {
        status: 'fulfilled',
        value: 'Hello!',
      },
      {
        status: 'rejected',
        reason: new Error('Not Found'),
      },
    ]
));
40.5.5.3 Eine einfache Implementierung von Promise.allSettled()

Dies ist eine vereinfachte Implementierung von Promise.allSettled() (z. B. führt sie keine Sicherheitsprüfungen durch)

function allSettled(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    function addElementToResult(i, elem) {
      result[i] = elem;
      elementCount++;
      if (elementCount === result.length) {
        resolve(result);
      }
    }

    let index = 0;
    for (const promise of iterable) {
      // Capture the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => addElementToResult(
          currentIndex, {
            status: 'fulfilled',
            value
          }),
        (reason) => addElementToResult(
          currentIndex, {
            status: 'rejected',
            reason
          }));
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

40.5.6 Short-Circuiting (fortgeschritten)

Für einen Promise-Kombinator bedeutet Short-Circuiting, dass der Output-Promise vorzeitig beendet wird – bevor alle Input-Promises beendet sind. Die folgenden Kombinatoren führen ein Short-Circuiting durch

Auch hier bedeutet ein früheres Beenden nicht, dass die Operationen hinter den ignorierten Promises gestoppt werden. Es bedeutet nur, dass ihre Beendigungen ignoriert werden.

40.6 Nebenläufigkeit und Promise.all() (fortgeschritten)

40.6.1 Sequentielle Ausführung vs. nebenläufige Ausführung

Betrachten Sie den folgenden Code

const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');

asyncFunc1()
  .then(result1 => {
    assert.equal(result1, 'one');
    return asyncFunc2();
  })
  .then(result2 => {
    assert.equal(result2, 'two');
  });

Die Verwendung von .then() auf diese Weise führt Promise-basierte Funktionen sequentiell aus: Erst nachdem das Ergebnis von asyncFunc1() beendet wurde, wird asyncFunc2() ausgeführt.

Promise.all() hilft bei der nebenläufigeren Ausführung von Promise-basierten Funktionen

Promise.all([asyncFunc1(), asyncFunc2()])
  .then(arr => {
    assert.deepEqual(arr, ['one', 'two']);
  });

40.6.2 Tipp zur Nebenläufigkeit: Konzentrieren Sie sich darauf, wann Operationen beginnen

Tipp zur Bestimmung, wie „nebenläufig“ asynchroner Code ist: Konzentrieren Sie sich darauf, wann asynchrone Operationen beginnen, nicht darauf, wie ihre Promises behandelt werden.

Zum Beispiel führt jede der folgenden Funktionen asyncFunc1() und asyncFunc2() nebenläufig aus, da sie nahezu gleichzeitig gestartet werden.

function concurrentAll() {
  return Promise.all([asyncFunc1(), asyncFunc2()]);
}

function concurrentThen() {
  const p1 = asyncFunc1();
  const p2 = asyncFunc2();
  return p1.then(r1 => p2.then(r2 => [r1, r2]));
}

Auf der anderen Seite führen beide der folgenden Funktionen asyncFunc1() und asyncFunc2() sequentiell aus: asyncFunc2() wird erst aufgerufen, nachdem das Promise von asyncFunc1() erfüllt wurde.

function sequentialThen() {
  return asyncFunc1()
    .then(r1 => asyncFunc2()
      .then(r2 => [r1, r2]));
}

function sequentialAll() {
  const p1 = asyncFunc1();
  const p2 = p1.then(() => asyncFunc2());
  return Promise.all([p1, p2]);
}

40.6.3 Promise.all() ist Fork-Join

Promise.all() steht in losem Zusammenhang mit dem Nebenläufigkeitsmuster „Fork-Join“. Betrachten wir noch einmal ein Beispiel, das wir zuvor angetroffen haben.

Promise.all([
    // (A) fork
    downloadText('http://example.com/first.txt'),
    downloadText('http://example.com/second.txt'),
  ])
  // (B) join
  .then(
    (arr) => assert.deepEqual(
      arr, ['First!', 'Second!']
    ));

40.7 Tipps zum Verketten von Promises

Dieser Abschnitt gibt Tipps zum Verketten von Promises.

40.7.1 Verkettungsfehler: Das Ende verlieren

Problem

// Don’t do this
function foo() {
  const promise = asyncFunc();
  promise.then(result => {
    // ···
  });

  return promise;
}

Die Berechnung beginnt mit dem Promise, das von asyncFunc() zurückgegeben wird. Aber danach wird die Berechnung fortgesetzt und über .then() ein weiteres Promise erstellt. foo() gibt das erstere Promise zurück, sollte aber das letztere zurückgeben. So beheben Sie das

function foo() {
  const promise = asyncFunc();
  return promise.then(result => {
    // ···
  });
}

40.7.2 Verkettungsfehler: Verschachtelung

Problem

// Don’t do this
asyncFunc1()
  .then(result1 => {
    return asyncFunc2()
    .then(result2 => { // (A)
      // ···
    });
  });

Das .then() in Zeile A ist verschachtelt. Eine flache Struktur wäre besser

asyncFunc1()
  .then(result1 => {
    return asyncFunc2();
  })
  .then(result2 => {
    // ···
  });

40.7.3 Verkettungsfehler: mehr Verschachtelung als nötig

Dies ist ein weiteres Beispiel für vermeidbare Verschachtelung

// Don’t do this
asyncFunc1()
  .then(result1 => {
    if (result1 < 0) {
      return asyncFuncA()
      .then(resultA => 'Result: ' + resultA);
    } else {
      return asyncFuncB()
      .then(resultB => 'Result: ' + resultB);
    }
  });

Wir können wieder eine flache Struktur erhalten

asyncFunc1()
  .then(result1 => {
    return result1 < 0 ? asyncFuncA() : asyncFuncB();
  })
  .then(resultAB => {
    return 'Result: ' + resultAB;
  });

40.7.4 Nicht jede Verschachtelung ist schlecht

Im folgenden Code profitieren wir tatsächlich von der Verschachtelung

db.open()
  .then(connection => { // (A)
    return connection.select({ name: 'Jane' })
      .then(result => { // (B)
        // Process result
        // Use `connection` to make more queries
      })
      // ···
      .finally(() => {
        connection.close(); // (C)
      });
  })

Wir erhalten in Zeile A ein asynchrones Ergebnis. In Zeile B verschachteln wir, damit wir in der Callback-Funktion und in Zeile C Zugriff auf die Variable connection haben.

40.7.5 Verkettungsfehler: Promises erstellen statt verketten

Problem

// Don’t do this
class Model {
  insertInto(db) {
    return new Promise((resolve, reject) => { // (A)
      db.insert(this.fields)
        .then(resultCode => {
          this.notifyObservers({event: 'created', model: this});
          resolve(resultCode);
        }).catch(err => {
          reject(err);
        })
    });
  }
  // ···
}

In Zeile A erstellen wir ein Promise, um das Ergebnis von db.insert() zu liefern. Das ist unnötig umständlich und kann vereinfacht werden

class Model {
  insertInto(db) {
    return db.insert(this.fields)
      .then(resultCode => {
        this.notifyObservers({event: 'created', model: this});
        return resultCode;
      });
  }
  // ···
}

Die Kernidee ist, dass wir kein Promise erstellen müssen; wir können das Ergebnis des .then()-Aufrufs zurückgeben. Ein zusätzlicher Vorteil ist, dass wir den Fehler von db.insert() nicht abfangen und erneut ablehnen müssen. Wir leiten seine Ablehnung einfach an den Aufrufer von .insertInto() weiter.

40.8 Schnellreferenz: Promise-Kombinatorfunktionen

Sofern nicht anders angegeben, wurde die Funktionalität in ECMAScript 6 eingeführt (zu diesem Zeitpunkt wurden Promises zur Sprache hinzugefügt).

Glossar

40.8.1 Promise.all()

Promise.all<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<T>>

40.8.2 Promise.race()

Promise.race<T>(promises: Iterable<Promise<T>>)
  : Promise<T>

40.8.3 Promise.any() [ES2021]

Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>

Dies ist die Typensignatur von AggregateError (einige Member wurden weggelassen)

class AggregateError {
  constructor(errors: Iterable<any>, message: string);
  get errors(): Array<any>;
  get message(): string;
}

40.8.4 Promise.allSettled() [ES2020]

Promise.allSettled<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<SettlementObject<T>>>

Dies ist die Typensignatur von SettlementObject

type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;

interface FulfillmentObject<T> {
  status: 'fulfilled';
  value: T;
}

interface RejectionObject {
  status: 'rejected';
  reason: unknown;
}