22. Generatoren
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

22. Generatoren



22.1 Übersicht

22.1.1 Was sind Generatoren?

Generatoren können als Prozesse (Code-Teile) betrachtet werden, die Sie anhalten und fortsetzen können

function* genFunc() {
    // (A)
    console.log('First');
    yield;
    console.log('Second');
}

Beachten Sie die neue Syntax: function* ist ein neues "Schlüsselwort" für *Generator-Funktionen* (es gibt auch *Generator-Methoden*). yield ist ein Operator, mit dem ein Generator sich selbst anhalten kann. Zusätzlich können Generatoren über yield auch Eingaben empfangen und Ausgaben senden.

Wenn Sie eine Generator-Funktion genFunc() aufrufen, erhalten Sie ein *Generator-Objekt* genObj, mit dem Sie den Prozess steuern können

const genObj = genFunc();

Der Prozess ist anfangs in Zeile A angehalten. genObj.next() setzt die Ausführung fort, ein yield innerhalb von genFunc() hält die Ausführung an

genObj.next();
// Output: First
genObj.next();
// output: Second

22.1.2 Arten von Generatoren

Es gibt vier Arten von Generatoren

  1. Generator-Funktionsdeklarationen
     function* genFunc() { ··· }
     const genObj = genFunc();
    
  2. Generator-Funktionsausdrücke
     const genFunc = function* () { ··· };
     const genObj = genFunc();
    
  3. Generator-Methodendefinitionen in Objekt-Literalen
     const obj = {
         * generatorMethod() {
             ···
         }
     };
     const genObj = obj.generatorMethod();
    
  4. Generator-Methodendefinitionen in Klassen-Definitionen (Klassendeklarationen oder Klassenausdrücke)
     class MyClass {
         * generatorMethod() {
             ···
         }
     }
     const myInst = new MyClass();
     const genObj = myInst.generatorMethod();
    

22.1.3 Anwendungsfall: Implementierung von Iterables

Die von Generatoren zurückgegebenen Objekte sind iterierbar; jedes yield trägt zur Sequenz der iterierten Werte bei. Daher können Sie Generatoren verwenden, um Iterables zu implementieren, die von verschiedenen ES6-Sprachmechanismen konsumiert werden können: for-of-Schleife, Spread-Operator (...) usw.

Die folgende Funktion gibt ein Iterable über die Eigenschaften eines Objekts zurück, wobei jedes [Schlüssel, Wert]-Paar eine Eigenschaft darstellt

function* objectEntries(obj) {
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        // `yield` returns a value and then pauses
        // the generator. Later, execution continues
        // where it was previously paused.
        yield [propKey, obj[propKey]];
    }
}

objectEntries() wird wie folgt verwendet

const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

Wie genau objectEntries() funktioniert, wird in einem eigenen Abschnitt erklärt. Die Implementierung der gleichen Funktionalität ohne Generatoren ist wesentlich aufwendiger.

22.1.4 Anwendungsfall: einfacherer asynchroner Code

Sie können Generatoren verwenden, um die Arbeit mit Promises erheblich zu vereinfachen. Betrachten wir eine Promise-basierte Funktion fetchJson() und wie sie mit Generatoren verbessert werden kann.

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}

Mit der Bibliothek co und einem Generator sieht dieser asynchrone Code synchron aus

const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});

ECMAScript 2017 wird Async-Funktionen haben, die intern auf Generatoren basieren. Mit ihnen sieht der Code so aus

async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}

Alle Versionen können wie folgt aufgerufen werden

fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

22.1.5 Anwendungsfall: Empfang von asynchronen Daten

Generatoren können Eingaben von next() über yield empfangen. Das bedeutet, dass Sie einen Generator aufwecken können, sobald neue Daten asynchron eintreffen, und für den Generator fühlt es sich an, als ob er die Daten synchron empfängt.

22.2 Was sind Generatoren?

*Generatoren* sind Funktionen, die angehalten und fortgesetzt werden können (denken Sie an kooperatives Multitasking oder Koroutinen), was eine Vielzahl von Anwendungen ermöglicht.

Als erstes Beispiel betrachten wir die folgende Generator-Funktion mit dem Namen genFunc

function* genFunc() {
    // (A)
    console.log('First');
    yield; // (B)
    console.log('Second'); // (C)
}

Zwei Dinge unterscheiden genFunc von einer normalen Funktionsdeklaration

Der Aufruf von genFunc führt seinen Körper nicht aus. Stattdessen erhalten Sie ein sogenanntes *Generator-Objekt*, mit dem Sie die Ausführung des Körpers steuern können

> const genObj = genFunc();

genFunc() wird zunächst vor dem Körper (Zeile A) suspendiert. Der Methodenaufruf genObj.next() setzt die Ausführung bis zum nächsten yield fort

> genObj.next()
First
{ value: undefined, done: false }

Wie Sie in der letzten Zeile sehen können, gibt genObj.next() auch ein Objekt zurück. Ignorieren wir das vorerst. Es wird später wichtig sein.

genFunc ist nun in Zeile B angehalten. Wenn wir next() erneut aufrufen, wird die Ausführung fortgesetzt und Zeile C ausgeführt

> genObj.next()
Second
{ value: undefined, done: true }

Danach ist die Funktion beendet, die Ausführung hat den Körper verlassen, und weitere Aufrufe von genObj.next() haben keine Wirkung.

22.2.1 Rollen von Generatoren

Generatoren können drei Rollen spielen

  1. Iteratoren (Datenerzeuger): Jedes yield kann über next() einen Wert zurückgeben, was bedeutet, dass Generatoren Sequenzen von Werten über Schleifen und Rekursion erzeugen können. Da Generator-Objekte das Interface Iterable implementieren (was im Kapitel über Iteration erklärt wird), können diese Sequenzen von jedem ECMAScript 6-Konstrukt verarbeitet werden, das Iterables unterstützt. Zwei Beispiele sind: for-of-Schleifen und der Spread-Operator (...).
  2. Beobachter (Datenkonsumenten): yield kann auch einen Wert von next() empfangen (über einen Parameter). Das bedeutet, dass Generatoren zu Datenkonsumenten werden, die pausieren, bis ein neuer Wert über next() in sie hineingeschickt wird.
  3. Koroutinen (Datenproduzenten und -konsumenten): Da Generatoren pausierbar sind und sowohl Datenproduzenten als auch Datenkonsumenten sein können, ist nur wenig Aufwand nötig, um sie in Koroutinen (kooperativ multitaskingfähige Aufgaben) zu verwandeln.

Die nächsten Abschnitte bieten tiefere Erklärungen dieser Rollen.

22.3 Generatoren als Iteratoren (Datenerzeugung)

Wie bereits erklärt, können Generator-Objekte Datenproduzenten, Datenkonsumenten oder beides sein. Dieser Abschnitt betrachtet sie als Datenproduzenten, wobei sie sowohl die Interfaces Iterable als auch Iterator implementieren (siehe unten). Das bedeutet, dass das Ergebnis einer Generator-Funktion sowohl ein Iterable als auch ein Iterator ist. Das vollständige Interface von Generator-Objekten wird später gezeigt.

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

Ich habe die Methode return() des Interfaces Iterable weggelassen, da sie in diesem Abschnitt nicht relevant ist.

Eine Generator-Funktion erzeugt eine Sequenz von Werten über yield, ein Datenkonsument verbraucht diese Werte über die Iterator-Methode next(). Zum Beispiel erzeugt die folgende Generator-Funktion die Werte 'a' und 'b'

function* genFunc() {
    yield 'a';
    yield 'b';
}

Diese Interaktion zeigt, wie die generierten Werte über das Generator-Objekt genObj abgerufen werden

> const genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }
> genObj.next()
{ value: 'b', done: false }
> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }

22.3.1 Wege zur Iteration über einen Generator

Da Generator-Objekte iterierbar sind, können ES6-Sprachkonstrukte, die Iterables unterstützen, auf sie angewendet werden. Die folgenden drei sind besonders wichtig.

Erstens, die for-of-Schleife

for (const x of genFunc()) {
    console.log(x);
}
// Output:
// a
// b

Zweitens, der Spread-Operator (...), der iterierte Sequenzen in Elemente eines Arrays verwandelt (konsultieren Sie das Kapitel zur Parameterbehandlung für weitere Informationen zu diesem Operator)

const arr = [...genFunc()]; // ['a', 'b']

Drittens, Destrukturierung

> const [x, y] = genFunc();
> x
'a'
> y
'b'

22.3.2 Rückkehr aus einem Generator

Die vorherige Generator-Funktion enthielt kein explizites return. Ein implizites return ist gleichbedeutend mit der Rückgabe von undefined. Betrachten wir einen Generator mit einem expliziten return

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'result';
}

Der zurückgegebene Wert erscheint im letzten Objekt, das von next() zurückgegeben wird, dessen Eigenschaft done true ist

> const genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }

Die meisten Konstrukte, die mit Iterables arbeiten, ignorieren jedoch den Wert im done-Objekt

for (const x of genFuncWithReturn()) {
    console.log(x);
}
// Output:
// a
// b

const arr = [...genFuncWithReturn()]; // ['a', 'b']

yield*, ein Operator zum Erstellen rekursiver Generatoraufrufe, berücksichtigt Werte in done-Objekten. Er wird später erklärt.

22.3.3 Werfen einer Ausnahme aus einem Generator

Wenn eine Ausnahme den Körper eines Generators verlässt, wirft next() diese

function* genFunc() {
    throw new Error('Problem!');
}
const genObj = genFunc();
genObj.next(); // Error: Problem!

Das bedeutet, dass next() drei verschiedene "Ergebnisse" liefern kann

22.3.4 Beispiel: Iteration über Eigenschaften

Betrachten wir ein Beispiel, das zeigt, wie praktisch Generatoren für die Implementierung von Iterables sind. Die folgende Funktion, objectEntries(), gibt ein Iterable über die Eigenschaften eines Objekts zurück

function* objectEntries(obj) {
    // In ES6, you can use strings or symbols as property keys,
    // Reflect.ownKeys() retrieves both
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        yield [propKey, obj[propKey]];
    }
}

Diese Funktion ermöglicht es Ihnen, über die Eigenschaften eines Objekts jane mittels der for-of-Schleife zu iterieren

const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe

Zum Vergleich – eine Implementierung von objectEntries(), die keine Generatoren verwendet, ist wesentlich komplizierter

function objectEntries(obj) {
    let index = 0;
    let propKeys = Reflect.ownKeys(obj);

    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (index < propKeys.length) {
                let key = propKeys[index];
                index++;
                return { value: [key, obj[key]] };
            } else {
                return { done: true };
            }
        }
    };
}

22.3.5 Nur in Generatoren kann man yield verwenden

Eine wesentliche Einschränkung von Generatoren ist, dass Sie nur yield verwenden können, wenn Sie sich (statisch) innerhalb einer Generator-Funktion befinden. Das heißt, das yield in Callbacks funktioniert nicht

function* genFunc() {
    ['a', 'b'].forEach(x => yield x); // SyntaxError
}

yield ist in Nicht-Generator-Funktionen nicht erlaubt, weshalb der vorherige Code einen Syntaxfehler verursacht. In diesem Fall ist es einfach, den Code neu zu schreiben, sodass er keine Callbacks verwendet (wie unten gezeigt). Aber leider ist das nicht immer möglich.

function* genFunc() {
    for (const x of ['a', 'b']) {
        yield x; // OK
    }
}

Der Vorteil dieser Einschränkung wird später erklärt: Er erleichtert die Implementierung von Generatoren und macht sie mit Event-Loops kompatibel.

22.3.6 Rekursion über yield*

Sie können yield nur innerhalb einer Generator-Funktion verwenden. Daher benötigen Sie eine Möglichkeit, einen Generator von einem anderen aus aufzurufen, wenn Sie einen rekursiven Algorithmus mit einem Generator implementieren möchten. Dieser Abschnitt zeigt, dass dies komplizierter ist, als es klingt, weshalb ES6 einen speziellen Operator, yield*, dafür hat. Vorerst erkläre ich nur, wie yield* funktioniert, wenn beide Generatoren Ausgaben erzeugen, später erkläre ich, wie die Dinge funktionieren, wenn Eingaben involviert sind.

Wie kann ein Generator einen anderen Generator rekursiv aufrufen? Nehmen wir an, Sie haben eine Generator-Funktion foo geschrieben

function* foo() {
    yield 'a';
    yield 'b';
}

Wie würden Sie foo von einer anderen Generator-Funktion bar aufrufen? Der folgende Ansatz funktioniert nicht!

function* bar() {
    yield 'x';
    foo(); // does nothing!
    yield 'y';
}

Der Aufruf von foo() gibt ein Objekt zurück, führt aber foo() nicht tatsächlich aus. Deshalb hat ECMAScript 6 den Operator yield* für rekursive Generatoraufrufe

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

// Collect all values yielded by bar() in an array
const arr = [...bar()];
    // ['x', 'a', 'b', 'y']

Intern funktioniert yield* ungefähr so

function* bar() {
    yield 'x';
    for (const value of foo()) {
        yield value;
    }
    yield 'y';
}

Der Operand von yield* muss kein Generator-Objekt sein, es kann jedes Iterable sein

function* bla() {
    yield 'sequence';
    yield* ['of', 'yielded'];
    yield 'values';
}

const arr = [...bla()];
    // ['sequence', 'of', 'yielded', 'values']
22.3.6.1 yield* berücksichtigt Ende-der-Iteration-Werte

Die meisten Konstrukte, die Iterables unterstützen, ignorieren den Wert, der im Ende-der-Iteration-Objekt enthalten ist (dessen Eigenschaft done true ist). Generatoren stellen diesen Wert über return bereit. Das Ergebnis von yield* ist der Ende-der-Iteration-Wert

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'The result';
}
function* logReturned(genObj) {
    const result = yield* genObj;
    console.log(result); // (A)
}

Wenn wir zu Zeile A gelangen wollen, müssen wir zuerst alle von logReturned() generierten Werte iterieren

> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]
22.3.6.2 Iteration über Bäume

Die Iteration über einen Baum mit Rekursion ist einfach, das Schreiben eines Iterators für einen Baum mit herkömmlichen Mitteln ist kompliziert. Deshalb glänzen Generatoren hier: Sie ermöglichen es Ihnen, einen Iterator per Rekursion zu implementieren. Als Beispiel betrachten wir die folgende Datenstruktur für binäre Bäume. Sie ist iterierbar, da sie eine Methode mit dem Schlüssel Symbol.iterator hat. Diese Methode ist eine Generator-Methode und gibt bei Aufruf einen Iterator zurück.

class BinaryTree {
    constructor(value, left=null, right=null) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    /** Prefix iteration */
    * [Symbol.iterator]() {
        yield this.value;
        if (this.left) {
            yield* this.left;
            // Short for: yield* this.left[Symbol.iterator]()
        }
        if (this.right) {
            yield* this.right;
        }
    }
}

Der folgende Code erstellt einen binären Baum und iteriert darüber mittels for-of

const tree = new BinaryTree('a',
    new BinaryTree('b',
        new BinaryTree('c'),
        new BinaryTree('d')),
    new BinaryTree('e'));

for (const x of tree) {
    console.log(x);
}
// Output:
// a
// b
// c
// d
// e

22.4 Generatoren als Beobachter (Datenkonsum)

Als Datenkonsumenten entsprechen Generator-Objekte der zweiten Hälfte des Generator-Interfaces, Observer

interface Observer {
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

Als Beobachter pausiert ein Generator, bis er Eingaben erhält. Es gibt drei Arten von Eingaben, die über die vom Interface spezifizierten Methoden gesendet werden

22.4.1 Senden von Werten über next()

Wenn Sie einen Generator als Beobachter verwenden, senden Sie Werte über next() an ihn, und er empfängt diese Werte über yield

function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`); // (A)
    console.log(`2. ${yield}`);
    return 'result';
}

Verwenden wir diesen Generator interaktiv. Zuerst erstellen wir ein Generator-Objekt

> const genObj = dataConsumer();

Wir rufen nun genObj.next() auf, was den Generator startet. Die Ausführung wird bis zum ersten yield fortgesetzt, wo der Generator pausiert. Das Ergebnis von next() ist der in Zeile A generierte Wert (undefined, da yield keinen Operanden hat). In diesem Abschnitt interessieren wir uns nicht für das, was next() zurückgibt, da wir es nur zum Senden von Werten verwenden, nicht zum Abrufen von Werten.

> genObj.next()
Started
{ value: undefined, done: false }

Wir rufen next() noch zweimal auf, um den Wert 'a' an das erste yield und den Wert 'b' an das zweite yield zu senden

> genObj.next('a')
1. a
{ value: undefined, done: false }

> genObj.next('b')
2. b
{ value: 'result', done: true }

Das Ergebnis des letzten next() ist der Wert, der von dataConsumer() zurückgegeben wird. done ist true, was bedeutet, dass der Generator beendet ist.

Leider ist next() asymmetrisch, aber das lässt sich nicht ändern: Es sendet immer einen Wert an das aktuell suspendierte yield, gibt aber den Operanden des nachfolgenden yield zurück.

22.4.1.1 Der erste next()

Wenn Sie einen Generator als Beobachter verwenden, ist es wichtig zu beachten, dass der einzige Zweck des ersten Aufrufs von next() darin besteht, den Beobachter zu starten. Erst danach ist er für Eingaben bereit, da dieser erste Aufruf die Ausführung bis zum ersten yield vorantreibt. Daher werden alle Eingaben, die Sie über den ersten next() senden, ignoriert

function* gen() {
    // (A)
    while (true) {
        const input = yield; // (B)
        console.log(input);
    }
}
const obj = gen();
obj.next('a');
obj.next('b');

// Output:
// b

Ursprünglich ist die Ausführung in Zeile A pausiert. Der erste Aufruf von next()

Der zweite Aufruf von next()

Die folgende Hilfsfunktion behebt dieses Problem

/**
 * Returns a function that, when called,
 * returns a generator object that is immediately
 * ready for input via `next()`
 */
function coroutine(generatorFunction) {
    return function (...args) {
        const generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
    };
}

Um zu sehen, wie coroutine() funktioniert, vergleichen wir einen gewrappten Generator mit einem normalen Generator

const wrapped = coroutine(function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
});
const normal = function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
};

Der gewrappte Generator ist sofort bereit für Eingaben

> wrapped().next('hello!')
First input: hello!

Der normale Generator benötigt ein zusätzliches next(), bis er für Eingaben bereit ist

> const genObj = normal();
> genObj.next()
{ value: undefined, done: false }
> genObj.next('hello!')
First input: hello!
{ value: 'DONE', done: true }

22.4.2 yield bindet locker

yield bindet sehr locker, so dass wir seinen Operanden nicht in Klammern setzen müssen

yield a + b + c;

Dies wird behandelt als

yield (a + b + c);

Nicht als

(yield a) + b + c;

Daher binden viele Operatoren fester als yield, und Sie müssen yield in Klammern setzen, wenn Sie es als Operanden verwenden möchten. Zum Beispiel erhalten Sie einen Syntaxfehler, wenn Sie ein nicht in Klammern gesetztes yield als Operanden von Plus verwenden

console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK

Sie benötigen keine Klammern, wenn yield ein direkter Parameter in einem Funktions- oder Methodenaufruf ist

foo(yield 'a', yield 'b');

Sie benötigen auch keine Klammern, wenn Sie yield auf der rechten Seite einer Zuweisung verwenden

const input = yield;
22.4.2.1 yield in der ES6-Grammatik

Die Notwendigkeit von Klammern um yield ist in den folgenden Grammatikregeln in der ECMAScript 6 Spezifikation zu sehen. Diese Regeln beschreiben, wie Ausdrücke geparst werden. Ich liste sie hier von allgemein (lockere Bindung, niedrige Priorität) zu spezifisch (enge Bindung, hohe Priorität). Wo immer eine bestimmte Art von Ausdruck gefordert ist, können Sie auch spezifischere verwenden. Das Gegenteil ist nicht der Fall. Die Hierarchie endet mit ParenthesizedExpression, was bedeutet, dass Sie jeden Ausdruck überall erwähnen können, wenn Sie ihn in Klammern setzen.

Expression :
    AssignmentExpression
    Expression , AssignmentExpression
AssignmentExpression :
    ConditionalExpression
    YieldExpression
    ArrowFunction
    LeftHandSideExpression = AssignmentExpression
    LeftHandSideExpression AssignmentOperator AssignmentExpression

···

AdditiveExpression :
    MultiplicativeExpression
    AdditiveExpression + MultiplicativeExpression
    AdditiveExpression - MultiplicativeExpression
MultiplicativeExpression :
    UnaryExpression
    MultiplicativeExpression MultiplicativeOperator UnaryExpression

···

PrimaryExpression :
    this
    IdentifierReference
    Literal
    ArrayLiteral
    ObjectLiteral
    FunctionExpression
    ClassExpression
    GeneratorExpression
    RegularExpressionLiteral
    TemplateLiteral
    ParenthesizedExpression
ParenthesizedExpression :
    ( Expression )

Die Operanden eines AdditiveExpression sind ein AdditiveExpression und ein MultiplicativeExpression. Daher ist die Verwendung eines (spezifischeren) ParenthesizedExpression als Operand in Ordnung, aber die Verwendung eines (allgemeineren) YieldExpression nicht.

22.4.3 return() und throw()

Generator-Objekte haben zwei zusätzliche Methoden, return() und throw(), die next() ähneln.

Fassen wir zusammen, wie next(x) funktioniert (nach dem ersten Aufruf)

  1. Der Generator ist derzeit bei einem yield-Operator suspendiert.
  2. Senden Sie den Wert x an dieses yield, was bedeutet, dass es zu x ausgewertet wird.
  3. Fahren Sie mit dem nächsten yield, return oder throw fort
    • yield x führt dazu, dass next() mit { value: x, done: false } zurückkehrt
    • return x führt dazu, dass next() mit { value: x, done: true } zurückkehrt
    • throw err (nicht innerhalb des Generators abgefangen) führt dazu, dass next() err wirft.

return() und throw() funktionieren ähnlich wie next(), aber sie tun etwas anderes in Schritt 2

22.4.4 return() beendet den Generator

return() führt ein return an der Stelle des yield aus, das zur letzten Suspension des Generators geführt hat. Betrachten wir die folgende Generator-Funktion, um zu sehen, wie das funktioniert.

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } finally {
        console.log('Exiting');
    }
}

In der folgenden Interaktion verwenden wir zunächst next(), um den Generator zu starten und bis zum yield in Zeile A fortzufahren. Dann kehren wir von dieser Stelle über return() zurück.

> const genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }
22.4.4.1 Verhindern der Beendigung

Sie können verhindern, dass return() den Generator beendet, wenn Sie innerhalb der finally-Klausel yielden (eine return-Anweisung in dieser Klausel ist ebenfalls möglich)

function* genFunc2() {
    try {
        console.log('Started');
        yield;
    } finally {
        yield 'Not done, yet!';
    }
}

Diesmal beendet return() die Generator-Funktion nicht. Dementsprechend ist die Eigenschaft done des von ihr zurückgegebenen Objekts false.

> const genObj2 = genFunc2();

> genObj2.next()
Started
{ value: undefined, done: false }

> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }

Sie können next() noch einmal aufrufen. Ähnlich wie bei Nicht-Generator-Funktionen ist der Rückgabewert der Generator-Funktion der Wert, der vor dem Eintritt in die finally-Klausel in die Warteschlange gestellt wurde.

> genObj2.next()
{ value: 'Result', done: true }
22.4.4.2 Rückkehr aus einem neuen Generator

Das Zurückgeben eines Wertes aus einem *neuen* Generator (der noch nicht gestartet wurde) ist erlaubt

> function* genFunc() {}
> genFunc().return('yes')
{ value: 'yes', done: true }

22.4.5 throw() signalisiert einen Fehler

throw() wirft eine Ausnahme an der Stelle des yield, das zur letzten Suspension des Generators geführt hat. Betrachten wir, wie das über die folgende Generator-Funktion funktioniert.

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } catch (error) {
        console.log('Caught: ' + error);
    }
}

In der folgenden Interaktion verwenden wir zunächst next(), um den Generator zu starten und bis zum yield in Zeile A fortzufahren. Dann werfen wir von dieser Stelle eine Ausnahme.

> const genObj1 = genFunc1();

> genObj1.next()
Started
{ value: undefined, done: false }

> genObj1.throw(new Error('Problem!'))
Caught: Error: Problem!
{ value: undefined, done: true }

Das Ergebnis von throw() (in der letzten Zeile gezeigt) ergibt sich aus dem Verlassen der Funktion mit einem impliziten return.

22.4.5.1 Werfen aus einem neuen Generator

Das Werfen einer Ausnahme in einem *neuen* Generator (der noch nicht gestartet wurde) ist erlaubt

> function* genFunc() {}
> genFunc().throw(new Error('Problem!'))
Error: Problem!

22.4.6 Beispiel: Verarbeitung von asynchron empfangenen Daten

Die Tatsache, dass Generatoren als Beobachter pausieren, während sie auf Eingaben warten, macht sie perfekt für die bedarfsgesteuerte Verarbeitung von Daten, die asynchron empfangen werden. Das Muster zum Einrichten einer Kette von Generatoren zur Verarbeitung ist wie folgt

Die gesamte Kette wird von einer Nicht-Generator-Funktion vorangestellt, die eine asynchrone Anfrage stellt und die Ergebnisse über next() in die Generator-Kette einspeist.

Als Beispiel verketten wir Generatoren, um eine Datei zu verarbeiten, die asynchron gelesen wird.

Der folgende Code richtet die Kette ein: er enthält die Generatoren splitLines, numberLines und printLines. Daten werden über die Nicht-Generator-Funktion readFile in die Kette eingespeist.

readFile(fileName, splitLines(numberLines(printLines())));

Ich werde erklären, was diese Funktionen tun, wenn ich ihren Code zeige.

Wie bereits erklärt, wenn Generatoren Eingaben über yield empfangen, tut der erste Aufruf von next() auf dem Generator-Objekt nichts. Deshalb verwende ich die zuvor gezeigte Hilfsfunktion coroutine(), um hier Koroutinen zu erstellen. Sie führt den ersten next() für uns aus.

readFile() ist die Nicht-Generator-Funktion, die alles startet

import {createReadStream} from 'fs';

/**
 * Creates an asynchronous ReadStream for the file whose name
 * is `fileName` and feeds it to the generator object `target`.
 *
 * @see ReadStream https://nodejs.org/api/fs.html#fs_class_fs_readstream
 */
function readFile(fileName, target) {
    const readStream = createReadStream(fileName,
        { encoding: 'utf8', bufferSize: 1024 });
    readStream.on('data', buffer => {
        const str = buffer.toString('utf8');
        target.next(str);
    });
    readStream.on('end', () => {
        // Signal end of output sequence
        target.return();
    });
}

Die Generator-Kette beginnt mit splitLines

/**
 * Turns a sequence of text chunks into a sequence of lines
 * (where lines are separated by newlines)
 */
const splitLines = coroutine(function* (target) {
    let previous = '';
    try {
        while (true) {
            previous += yield;
            let eolIndex;
            while ((eolIndex = previous.indexOf('\n')) >= 0) {
                const line = previous.slice(0, eolIndex);
                target.next(line);
                previous = previous.slice(eolIndex+1);
            }
        }
    } finally {
        // Handle the end of the input sequence
        // (signaled via `return()`)
        if (previous.length > 0) {
            target.next(previous);
        }
        // Signal end of output sequence
        target.return();
    }
});

Beachten Sie ein wichtiges Muster

Der nächste Generator ist numberLines

//**
 * Prefixes numbers to a sequence of lines
 */
const numberLines = coroutine(function* (target) {
    try {
        for (const lineNo = 0; ; lineNo++) {
            const line = yield;
            target.next(`${lineNo}: ${line}`);
        }
    } finally {
        // Signal end of output sequence
        target.return();
    }
});

Der letzte Generator ist printLines

/**
 * Receives a sequence of lines (without newlines)
 * and logs them (adding newlines).
 */
const printLines = coroutine(function* () {
    while (true) {
        const line = yield;
        console.log(line);
    }
});

Das Geniale an diesem Code ist, dass alles faul (bei Bedarf) geschieht: Zeilen werden gesplittet, nummeriert und gedruckt, sobald sie ankommen; wir müssen nicht warten, bis der gesamte Text da ist, bevor wir mit dem Drucken beginnen können.

22.4.7 yield*: die ganze Geschichte

Als grobe Faustregel führt yield* einen Funktionsaufruf (in etwa) von einem Generator (dem *Aufrufer*) zu einem anderen Generator (dem *Aufgerufenen*) durch.

Bisher haben wir nur einen Aspekt von yield gesehen: Er leitet generierte Werte vom Aufgerufenen zum Aufrufer weiter. Da wir uns nun für Generatoren interessieren, die Eingaben empfangen, wird ein weiterer Aspekt relevant: yield* leitet auch Eingaben, die vom Aufrufer empfangen werden, an den Aufgerufenen weiter. In gewisser Weise wird der Aufgerufene zum aktiven Generator und kann über das Generator-Objekt des Aufrufers gesteuert werden.

22.4.7.1 Beispiel: yield* leitet next() weiter

Die folgende Generator-Funktion caller() ruft die Generator-Funktion callee() über yield* auf.

function* callee() {
    console.log('callee: ' + (yield));
}
function* caller() {
    while (true) {
        yield* callee();
    }
}

callee protokolliert Werte, die über next() empfangen werden, was es uns ermöglicht zu überprüfen, ob er die Werte 'a' und 'b' empfängt, die wir an caller senden.

> const callerObj = caller();

> callerObj.next() // start
{ value: undefined, done: false }

> callerObj.next('a')
callee: a
{ value: undefined, done: false }

> callerObj.next('b')
callee: b
{ value: undefined, done: false }

throw() und return() werden auf ähnliche Weise weitergeleitet.

22.4.7.2 Die Semantik von yield* in JavaScript ausgedrückt

Ich werde die vollständige Semantik von yield* erläutern, indem ich zeige, wie man sie in JavaScript implementieren würde.

Die folgende Anweisung

let yieldStarResult = yield* calleeFunc();

ist ungefähr äquivalent zu

let yieldStarResult;

const calleeObj = calleeFunc();
let prevReceived = undefined;
while (true) {
    try {
        // Forward input previously received
        const {value,done} = calleeObj.next(prevReceived);
        if (done) {
            yieldStarResult = value;
            break;
        }
        prevReceived = yield value;
    } catch (e) {
        // Pretend `return` can be caught like an exception
        if (e instanceof Return) {
            // Forward input received via return()
            calleeObj.return(e.returnedValue);
            return e.returnedValue; // “re-throw”
        } else {
            // Forward input received via throw()
            calleeObj.throw(e); // may throw
        }
    }
}

Um die Dinge einfach zu halten, fehlen in diesem Code einige Dinge

22.5 Generatoren als Koroutinen (kooperatives Multitasking)

Wir haben Generatoren entweder als Quellen oder als Senken von Daten gesehen. Für viele Anwendungen ist es eine gute Praxis, diese beiden Rollen streng zu trennen, da dies die Dinge einfacher hält. Dieser Abschnitt beschreibt das vollständige Generator-Interface (das beide Rollen kombiniert) und einen Anwendungsfall, bei dem beide Rollen benötigt werden: kooperatives Multitasking, bei dem Aufgaben sowohl Informationen senden als auch empfangen müssen.

22.5.1 Das vollständige Generator-Interface

Das vollständige Interface von Generator-Objekten, Generator, behandelt sowohl Ausgabe als auch Eingabe

interface Generator {
    next(value? : any) : IteratorResult;
    throw(value? : any) : IteratorResult;
    return(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

Das Interface Generator kombiniert zwei Interfaces, die wir bereits gesehen haben: Iterator für die Ausgabe und Observer für die Eingabe.

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

22.5.2 Kooperatives Multitasking

Kooperatives Multitasking ist eine Anwendung von Generatoren, bei der wir sie benötigen, um sowohl Ausgabe als auch Eingabe zu verarbeiten. Bevor wir uns damit befassen, wie das funktioniert, lassen Sie uns zunächst den aktuellen Stand der Parallelität in JavaScript überprüfen.

JavaScript läuft in einem einzigen Prozess. Es gibt zwei Möglichkeiten, wie diese Einschränkung aufgehoben wird

Zwei Anwendungsfälle profitieren von kooperativem Multitasking, da sie Kontrollflüsse beinhalten, die sowieso meist sequenziell sind, mit gelegentlichen Pausen

22.5.2.1 Vereinfachung asynchroner Berechnungen über Generatoren

Mehrere Promise-basierte Bibliotheken vereinfachen asynchronen Code über Generatoren. Generatoren eignen sich ideal als Clients für Promises, da sie bis zum Eintreffen eines Ergebnisses suspendiert werden können.

Das folgende Beispiel zeigt, wie das aussieht, wenn man die Bibliothek co von T.J. Holowaychuk verwendet. Wir benötigen zwei Bibliotheken (wenn wir Node.js-Code über babel-node ausführen)

import fetch from 'isomorphic-fetch';
const co = require('co');

co ist die eigentliche Bibliothek für kooperatives Multitasking, isomorphic-fetch ist ein Polyfill für die neue Promise-basierte fetch API (ein Ersatz für XMLHttpRequest; lesen Sie „That’s so fetch!“ von Jake Archibald für weitere Informationen). fetch erleichtert das Schreiben einer Funktion getFile, die den Text einer Datei an einer url über ein Promise zurückgibt.

function getFile(url) {
    return fetch(url)
        .then(request => request.text());
}

Wir haben nun alle Zutaten, um co zu verwenden. Die folgende Aufgabe liest die Texte zweier Dateien, parst das darin enthaltene JSON und gibt das Ergebnis aus.

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('https://:8000/croft.json'),
            getFile('https://:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

Beachten Sie, wie synchron dieser Code aussieht, obwohl er in Zeile A einen asynchronen Aufruf tätigt. Ein Generator als Aufgabe tätigt einen asynchronen Aufruf, indem er ein Promise an die Scheduler-Funktion co übergibt. Das Übergeben pausiert den Generator. Sobald das Promise ein Ergebnis zurückgibt, setzt der Scheduler den Generator fort, indem er ihm das Ergebnis über next() übergibt. Eine einfache Version von co sieht wie folgt aus.

function co(genFunc) {
    const genObj = genFunc();
    step(genObj.next());

    function step({value,done}) {
        if (!done) {
            // A Promise was yielded
            value
            .then(result => {
                step(genObj.next(result)); // (A)
            })
            .catch(error => {
                step(genObj.throw(error)); // (B)
            });
        }
    }
}

Ich habe ignoriert, dass next() (Zeile A) und throw() (Zeile B) Ausnahmen auslösen können (immer wenn eine Ausnahme den Körper der Generatorfunktion verlässt).

22.5.3 Die Grenzen des kooperativen Multitaskings über Generatoren

Coroutinen sind kooperativ gemanagte Aufgaben, die keine Einschränkungen haben: Innerhalb einer Coroutine kann jede Funktion die gesamte Coroutine suspendieren (die Funktionsaktivierung selbst, die Aktivierung des Aufrufers der Funktion, des Aufrufers des Aufrufers usw.).

Im Gegensatz dazu kann man einen Generator nur direkt aus einem Generator heraus suspendieren und nur die aktuelle Funktionsaktivierung wird suspendiert. Aufgrund dieser Einschränkungen werden Generatoren gelegentlich als flache Coroutinen [3] bezeichnet.

22.5.3.1 Die Vorteile der Einschränkungen von Generatoren

Die Einschränkungen von Generatoren haben zwei Hauptvorteile

JavaScript verfügt bereits über einen sehr einfachen Stil des kooperativen Multitaskings: die Ereignisschleife, die die Ausführung von Aufgaben in einer Warteschlange plant. Jede Aufgabe wird gestartet, indem eine Funktion aufgerufen wird, und ist abgeschlossen, sobald diese Funktion beendet ist. Ereignisse, setTimeout() und andere Mechanismen fügen der Warteschlange Aufgaben hinzu.

Dieser Multitasking-Stil garantiert eine wichtige Sache: bis zur Fertigstellung ausführen; jede Funktion kann darauf vertrauen, nicht von einer anderen Aufgabe unterbrochen zu werden, bis sie abgeschlossen ist. Funktionen werden zu Transaktionen und können vollständige Algorithmen ausführen, ohne dass jemand die Daten, mit denen sie arbeiten, in einem Zwischenzustand sieht. Gleichzeitiger Zugriff auf gemeinsame Daten erschwert Multitasking und ist im Nebenläufigkeitsmodell von JavaScript nicht erlaubt. Deshalb ist "bis zur Fertigstellung ausführen" etwas Gutes.

Leider verhindern Coroutinen das "bis zur Fertigstellung ausführen", da jede Funktion ihren Aufrufer suspendieren könnte. Zum Beispiel besteht der folgende Algorithmus aus mehreren Schritten

step1(sharedData);
step2(sharedData);
lastStep(sharedData);

Wenn step2 den Algorithmus suspendieren würde, könnten andere Aufgaben ausgeführt werden, bevor der letzte Schritt des Algorithmus ausgeführt wird. Diese Aufgaben könnten andere Teile der Anwendung enthalten, die sharedData in einem unvollständigen Zustand sehen würden. Generatoren bewahren das "bis zur Fertigstellung ausführen", sie suspendieren nur sich selbst und kehren zu ihrem Aufrufer zurück.

co und ähnliche Bibliotheken bieten Ihnen die meiste Leistung von Coroutinen, ohne deren Nachteile

22.6 Beispiele für Generatoren

Dieser Abschnitt gibt mehrere Beispiele dafür, wofür Generatoren verwendet werden können.

22.6.1 Implementierung von Iterables über Generatoren

In dem Kapitel über Iteration habe ich mehrere Iterables "von Hand" implementiert. In diesem Abschnitt verwende ich stattdessen Generatoren.

22.6.1.1 Der Iterable-Combinator take()

take() konvertiert eine (potenziell unendliche) Sequenz von iterierten Werten in eine Sequenz der Länge n

function* take(n, iterable) {
    for (const x of iterable) {
        if (n <= 0) return;
        n--;
        yield x;
    }
}

Das Folgende ist ein Beispiel für die Verwendung

const arr = ['a', 'b', 'c', 'd'];
for (const x of take(2, arr)) {
    console.log(x);
}
// Output:
// a
// b

Eine Implementierung von take() ohne Generatoren ist komplizierter

function take(n, iterable) {
    const iter = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (n > 0) {
                n--;
                return iter.next();
            } else {
                maybeCloseIterator(iter);
                return { done: true };
            }
        },
        return() {
            n = 0;
            maybeCloseIterator(iter);
        }
    };
}
function maybeCloseIterator(iterator) {
    if (typeof iterator.return === 'function') {
        iterator.return();
    }
}

Beachten Sie, dass der Iterable-Combinator zip() nicht viel davon profitiert, über einen Generator implementiert zu werden, da mehrere Iterables beteiligt sind und for-of nicht verwendet werden kann.

22.6.1.2 Unendliche Iterables

naturalNumbers() gibt ein Iterable über alle natürlichen Zahlen zurück

function* naturalNumbers() {
    for (let n=0;; n++) {
        yield n;
    }
}

Diese Funktion wird oft in Verbindung mit einem Combinator verwendet

for (const x of take(3, naturalNumbers())) {
    console.log(x);
}
// Output
// 0
// 1
// 2

Hier ist die Nicht-Generator-Implementierung, damit Sie vergleichen können

function naturalNumbers() {
    let n = 0;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return { value: n++ };
        }
    }
}
22.6.1.3 Array-inspirierte Iterable-Combinatoren: map, filter

Arrays können über die Methoden map und filter transformiert werden. Diese Methoden können verallgemeinert werden, um Iterables als Eingabe und Iterables als Ausgabe zu haben.

22.6.1.3.1 Ein verallgemeinertes map()

Dies ist die verallgemeinerte Version von map

function* map(iterable, mapFunc) {
    for (const x of iterable) {
        yield mapFunc(x);
    }
}

map() funktioniert mit unendlichen Iterables

> [...take(4, map(naturalNumbers(), x => x * x))]
[ 0, 1, 4, 9 ]
22.6.1.3.2 Ein verallgemeinertes filter()

Dies ist die verallgemeinerte Version von filter

function* filter(iterable, filterFunc) {
    for (const x of iterable) {
        if (filterFunc(x)) {
            yield x;
        }
    }
}

filter() funktioniert mit unendlichen Iterables

> [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))]
[ 0, 2, 4, 6 ]

22.6.2 Generatoren für lazy evaluation

Die nächsten beiden Beispiele zeigen, wie Generatoren zur Verarbeitung eines Zeichenstroms verwendet werden können.

Das Coole ist, dass alles lazy (inkrementell und nach Bedarf) berechnet wird: Die Berechnung beginnt, sobald das erste Zeichen eintrifft. Wir müssen zum Beispiel nicht warten, bis wir alle Zeichen haben, um das erste Wort zu erhalten.

22.6.2.1 Lazy Pull (Generatoren als Iteratoren)

Lazy Pull mit Generatoren funktioniert wie folgt. Die drei Generatoren, die die Schritte 1-3 implementieren, werden wie folgt verkettet

addNumbers(extractNumbers(tokenize(CHARS)))

Jedes Mitglied der Kette zieht Daten aus einer Quelle und gibt eine Sequenz von Elementen aus. Die Verarbeitung beginnt mit tokenize, dessen Quelle die Zeichenkette CHARS ist.

22.6.2.1.1 Schritt 1 – Tokenisierung

Der folgende Trick vereinfacht den Code etwas: Das Iterator-Ergebnis des Sequenzendes (dessen Eigenschaft done false ist) wird in den Sentinel-Wert END_OF_SEQUENCE konvertiert.

/**
 * Returns an iterable that transforms the input sequence
 * of characters into an output sequence of words.
 */
function* tokenize(chars) {
    const iterator = chars[Symbol.iterator]();
    let ch;
    do {
        ch = getNextItem(iterator); // (A)
        if (isWordChar(ch)) {
            let word = '';
            do {
                word += ch;
                ch = getNextItem(iterator); // (B)
            } while (isWordChar(ch));
            yield word; // (C)
        }
        // Ignore all other characters
    } while (ch !== END_OF_SEQUENCE);
}
const END_OF_SEQUENCE = Symbol();
function getNextItem(iterator) {
    const {value,done} = iterator.next();
    return done ? END_OF_SEQUENCE : value;
}
function isWordChar(ch) {
    return typeof ch === 'string' && /^[A-Za-z0-9]$/.test(ch);
}

Wie ist dieser Generator lazy? Wenn Sie ihn über next() nach einem Token fragen, zieht er seinen iterator (Zeilen A und B) so oft wie nötig, um einen Token zu erzeugen, und gibt dann diesen Token aus (Zeile C). Dann pausiert er, bis er erneut nach einem Token gefragt wird. Das bedeutet, dass die Tokenisierung beginnt, sobald die ersten Zeichen verfügbar sind, was für Streams praktisch ist.

Versuchen wir die Tokenisierung. Beachten Sie, dass die Leerzeichen und der Punkt keine Wörter sind. Sie werden ignoriert, trennen aber Wörter. Wir nutzen die Tatsache, dass Zeichenketten Iterables für Zeichen (Unicode-Codepunkte) sind. Das Ergebnis von tokenize() ist ein Iterable von Wörtern, das wir über den Spread-Operator (...) in ein Array umwandeln.

> [...tokenize('2 apples and 5 oranges.')]
[ '2', 'apples', 'and', '5', 'oranges' ]
22.6.2.1.2 Schritt 2 – Extrahieren von Zahlen

Dieser Schritt ist relativ einfach, wir geben nur Wörter aus, die nichts als Ziffern enthalten, nachdem wir sie mit Number() in Zahlen konvertiert haben.

/**
 * Returns an iterable that filters the input sequence
 * of words and only yields those that are numbers.
 */
function* extractNumbers(words) {
    for (const word of words) {
        if (/^[0-9]+$/.test(word)) {
            yield Number(word);
        }
    }
}

Sie können die Laziness wieder erkennen: Wenn Sie über next() nach einer Zahl fragen, erhalten Sie eine (über yield), sobald sie in words angetroffen wird.

Extrahieren wir die Zahlen aus einem Array von Wörtern

> [...extractNumbers(['hello', '123', 'world', '45'])]
[ 123, 45 ]

Beachten Sie, dass Zeichenketten in Zahlen umgewandelt werden.

22.6.2.1.3 Schritt 3 – Addieren von Zahlen
/**
 * Returns an iterable that contains, for each number in
 * `numbers`, the total sum of numbers encountered so far.
 * For example: 7, 4, -1 --> 7, 11, 10
 */
function* addNumbers(numbers) {
    let result = 0;
    for (const n of numbers) {
        result += n;
        yield result;
    }
}

Versuchen wir ein einfaches Beispiel

> [...addNumbers([5, -2, 12])]
[ 5, 3, 15 ]
22.6.2.1.4 Herausziehen der Ausgabe

Für sich allein erzeugt die Kette von Generatoren keine Ausgabe. Wir müssen die Ausgabe aktiv über den Spread-Operator herausziehen.

const CHARS = '2 apples and 5 oranges.';
const CHAIN = addNumbers(extractNumbers(tokenize(CHARS)));
console.log([...CHAIN]);
    // [ 2, 7 ]

Die Hilfsfunktion logAndYield ermöglicht es uns zu untersuchen, ob die Dinge tatsächlich lazy berechnet werden.

function* logAndYield(iterable, prefix='') {
    for (const item of iterable) {
        console.log(prefix + item);
        yield item;
    }
}

const CHAIN2 = logAndYield(addNumbers(extractNumbers(tokenize(logAndYield(CHA\
RS)))), '-> ');
[...CHAIN2];

// Output:
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .

Die Ausgabe zeigt, dass addNumbers ein Ergebnis produziert, sobald die Zeichen '2' und ' ' empfangen werden.

22.6.2.2 Lazy Push (Generatoren als Observables)

Es ist nicht viel Arbeit nötig, um den vorherigen Pull-basierten Algorithmus in einen Push-basierten umzuwandeln. Die Schritte sind die gleichen. Aber anstatt durch Ziehen zu enden, beginnen wir durch Drücken.

Wie bereits erklärt, wenn Generatoren Eingaben über yield empfangen, tut der erste Aufruf von next() auf dem Generator-Objekt nichts. Deshalb verwende ich die zuvor gezeigte Hilfsfunktion coroutine(), um hier Koroutinen zu erstellen. Sie führt den ersten next() für uns aus.

Die folgende Funktion send() erledigt das Drücken.

/**
 * Pushes the items of `iterable` into `sink`, a generator.
 * It uses the generator method `next()` to do so.
 */
function send(iterable, sink) {
    for (const x of iterable) {
        sink.next(x);
    }
    sink.return(); // signal end of stream
}

Wenn ein Generator einen Stream verarbeitet, muss er sich des Stream-Endes bewusst sein, damit er richtig aufräumen kann. Für Pull haben wir das über einen speziellen End-of-Stream-Sentinel gemacht. Für Push wird das Ende des Streams über return() signalisiert.

Testen wir send() über einen Generator, der einfach alles ausgibt, was er empfängt.

/**
 * This generator logs everything that it receives via `next()`.
 */
const logItems = coroutine(function* () {
    try {
        while (true) {
            const item = yield; // receive item via `next()`
            console.log(item);
        }
    } finally {
        console.log('DONE');
    }
});

Senden wir logItems() drei Zeichen über eine Zeichenkette (die ein Iterable von Unicode-Codepunkten ist).

> send('abc', logItems());
a
b
c
DONE
22.6.2.2.1 Schritt 1 – Tokenisierung

Beachten Sie, wie dieser Generator auf das Ende des Streams (signalisiert über return()) in zwei finally-Klauseln reagiert. Wir verlassen uns darauf, dass return() an eines der beiden yields gesendet wird. Andernfalls würde der Generator niemals enden, da die Endlosschleife, die in Zeile A beginnt, niemals enden würde.

/**
 * Receives a sequence of characters (via the generator object
 * method `next()`), groups them into words and pushes them
 * into the generator `sink`.
 */
const tokenize = coroutine(function* (sink) {
    try {
        while (true) { // (A)
            let ch = yield; // (B)
            if (isWordChar(ch)) {
                // A word has started
                let word = '';
                try {
                    do {
                        word += ch;
                        ch = yield; // (C)
                    } while (isWordChar(ch));
                } finally {
                    // The word is finished.
                    // We get here if
                    // - the loop terminates normally
                    // - the loop is terminated via `return()` in line C
                    sink.next(word); // (D)
                }
            }
            // Ignore all other characters
        }
    } finally {
        // We only get here if the infinite loop is terminated
        // via `return()` (in line B or C).
        // Forward `return()` to `sink` so that it is also
        // aware of the end of stream.
        sink.return();
    }
});

function isWordChar(ch) {
    return /^[A-Za-z0-9]$/.test(ch);
}

Dieses Mal wird die Laziness durch Push angetrieben: Sobald der Generator genügend Zeichen für ein Wort empfangen hat (in Zeile C), drückt er das Wort in sink (Zeile D). Das bedeutet, dass der Generator nicht wartet, bis er alle Zeichen empfangen hat.

tokenize() demonstriert, dass Generatoren gut als Implementierungen von linearen Zustandsautomaten funktionieren. In diesem Fall hat die Maschine zwei Zustände: "innerhalb eines Wortes" und "nicht innerhalb eines Wortes".

Lassen Sie uns eine Zeichenkette tokenisieren

> send('2 apples and 5 oranges.', tokenize(logItems()));
2
apples
and
5
oranges
22.6.2.2.2 Schritt 2 – Zahlen extrahieren

Dieser Schritt ist unkompliziert.

/**
 * Receives a sequence of strings (via the generator object
 * method `next()`) and pushes only those strings to the generator
 * `sink` that are “numbers” (consist only of decimal digits).
 */
const extractNumbers = coroutine(function* (sink) {
    try {
        while (true) {
            const word = yield;
            if (/^[0-9]+$/.test(word)) {
                sink.next(Number(word));
            }
        }
    } finally {
        // Only reached via `return()`, forward.
        sink.return();
    }
});

Die Dinge sind wieder lazy: Sobald eine Zahl angetroffen wird, wird sie an sink gedrückt.

Extrahieren wir die Zahlen aus einem Array von Wörtern

> send(['hello', '123', 'world', '45'], extractNumbers(logItems()));
123
45
DONE

Beachten Sie, dass die Eingabe eine Sequenz von Zeichenketten ist, während die Ausgabe eine Sequenz von Zahlen ist.

22.6.2.2.3 Schritt 3 – Zahlen addieren

Dieses Mal reagieren wir auf das Ende des Streams, indem wir einen einzelnen Wert ausgeben und dann den Sink schließen.

/**
 * Receives a sequence of numbers (via the generator object
 * method `next()`). For each number, it pushes the total sum
 * so far to the generator `sink`.
 */
const addNumbers = coroutine(function* (sink) {
    let sum = 0;
    try {
        while (true) {
            sum += yield;
            sink.next(sum);
        }
    } finally {
        // We received an end-of-stream
        sink.return(); // signal end of stream
    }
});

Lassen Sie uns diesen Generator ausprobieren

> send([5, -2, 12], addNumbers(logItems()));
5
3
15
DONE
22.6.2.2.4 Drücken der Eingabe

Die Kette von Generatoren beginnt mit tokenize und endet mit logItems, das alles protokolliert, was es empfängt. Wir drücken eine Sequenz von Zeichen über send in die Kette.

const INPUT = '2 apples and 5 oranges.';
const CHAIN = tokenize(extractNumbers(addNumbers(logItems())));
send(INPUT, CHAIN);

// Output
// 2
// 7
// DONE

Der folgende Code beweist, dass die Verarbeitung wirklich lazy erfolgt.

const CHAIN2 = tokenize(extractNumbers(addNumbers(logItems({ prefix: '-> ' })\
)));
send(INPUT, CHAIN2, { log: true });

// Output
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
// DONE

Die Ausgabe zeigt, dass addNumbers ein Ergebnis produziert, sobald die Zeichen '2' und ' ' gedrückt werden.

22.6.3 Kooperatives Multitasking über Generatoren

22.6.3.1 Pausieren langlaufender Aufgaben

In diesem Beispiel erstellen wir einen Zähler, der auf einer Webseite angezeigt wird. Wir verbessern eine Anfangsversion, bis wir eine kooperativ gemanagte Version haben, die den Hauptthread und die Benutzeroberfläche nicht blockiert.

Dies ist der Teil der Webseite, in dem der Zähler angezeigt werden soll.

<body>
    Counter: <span id="counter"></span>
</body>

Diese Funktion zeigt einen Zähler an, der für immer hochzählt5

function countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
    }
}

Wenn Sie diese Funktion ausführen würden, würde sie den Thread der Benutzeroberfläche, in dem sie läuft, vollständig blockieren und ihre Registerkarte würde nicht mehr reagieren.

Implementieren wir dieselbe Funktionalität über einen Generator, der periodisch über yield pausiert (eine Planungsfunktion zur Ausführung dieses Generators wird später gezeigt).

function* countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
        yield; // pause
    }
}

Fügen wir eine kleine Verbesserung hinzu. Wir verschieben die Aktualisierung der Benutzeroberfläche in einen anderen Generator, displayCounter, den wir über yield* aufrufen. Da es sich um einen Generator handelt, kann er auch das Pausieren übernehmen.

function* countUp(start = 0) {
    while (true) {
        start++;
        yield* displayCounter(start);
    }
}
function* displayCounter(counter) {
    const counterSpan = document.querySelector('#counter');
    counterSpan.textContent = String(counter);
    yield; // pause
}

Zuletzt ist dies eine Planungsfunktion, die wir zum Ausführen von countUp() verwenden können. Jeder Ausführungsschritt des Generators wird von einer separaten Aufgabe behandelt, die über setTimeout() erstellt wird. Das bedeutet, dass die Benutzeroberfläche zwischenzeitlich andere Aufgaben planen kann und reaktionsfähig bleibt.

function run(generatorObject) {
    if (!generatorObject.next().done) {
        // Add a new task to the event queue
        setTimeout(function () {
            run(generatorObject);
        }, 1000);
    }
}

Mit Hilfe von run erhalten wir einen (nahezu) unendlichen Zählvorgang, der die Benutzeroberfläche nicht blockiert.

run(countUp());
22.6.3.2 Kooperatives Multitasking mit Generatoren und Node.js-Style Callbacks

Wenn Sie eine Generatorfunktion (oder -methode) aufrufen, hat sie keinen Zugriff auf ihr Generatorobjekt; ihr this ist das this, das sie hätte, wenn sie eine Nicht-Generator-Funktion wäre. Eine Umgehung besteht darin, das Generatorobjekt über yield in die Generatorfunktion zu übergeben.

Das folgende Node.js-Skript verwendet diese Technik, wickelt das Generatorobjekt jedoch in einen Callback (next, Zeile A). Es muss über babel-node ausgeführt werden.

import {readFile} from 'fs';

const fileNames = process.argv.slice(2);

run(function* () {
    const next = yield;
    for (const f of fileNames) {
        const contents = yield readFile(f, { encoding: 'utf8' }, next);
        console.log('##### ' + f);
        console.log(contents);
    }
});

In Zeile A erhalten wir einen Callback, den wir mit Funktionen verwenden können, die den Node.js-Callback-Konventionen folgen. Der Callback verwendet das Generatorobjekt, um den Generator aufzuwecken, wie Sie in der Implementierung von run() sehen können.

function run(generatorFunction) {
    const generatorObject = generatorFunction();

    // Step 1: Proceed to first `yield`
    generatorObject.next();

    // Step 2: Pass in a function that the generator can use as a callback
    function nextFunction(error, result) {
        if (error) {
            generatorObject.throw(error);
        } else {
            generatorObject.next(result);
        }
    }
    generatorObject.next(nextFunction);

    // Subsequent invocations of `next()` are triggered by `nextFunction`
}
22.6.3.3 Communicating Sequential Processes (CSP)

Die Bibliothek js-csp bringt Communicating Sequential Processes (CSP) nach JavaScript, einen Stil des kooperativen Multitaskings, der ClojureScripts core.async und Gos Goroutinen ähnelt. js-csp hat zwei Abstraktionen.

Als Beispiel verwenden wir CSP zur Handhabung von DOM-Ereignissen, in einer Weise, die an Functional Reactive Programming erinnert. Der folgende Code verwendet die Funktion listen() (die später gezeigt wird), um einen Kanal zu erstellen, der mousemove-Ereignisse ausgibt. Er ruft dann kontinuierlich die Ausgabe über take ab, innerhalb einer Endlosschleife. Dank yield blockiert der Prozess, bis der Kanal eine Ausgabe hat.

import csp from 'js-csp';

csp.go(function* () {
    const element = document.querySelector('#uiElement1');
    const channel = listen(element, 'mousemove');
    while (true) {
        const event = yield csp.take(channel);
        const x = event.layerX || event.clientX;
        const y = event.layerY || event.clientY;
        element.textContent = `${x}, ${y}`;
    }
});

listen() wird wie folgt implementiert.

function listen(element, type) {
    const channel = csp.chan();
    element.addEventListener(type,
        event => {
            csp.putAsync(channel, event);
        });
    return channel;
}

22.7 Vererbung innerhalb der Iterations-API (einschließlich Generatoren)

Dies ist ein Diagramm, wie verschiedene Objekte in ECMAScript 6 miteinander verbunden sind (es basiert auf dem Diagramm von Allen Wirf-Brock in der ECMAScript-Spezifikation).

Legende

Das Diagramm enthüllt zwei interessante Fakten

Erstens verhält sich eine Generatorfunktion g sehr ähnlich wie ein Konstruktor (man kann sie jedoch nicht mit new aufrufen; das führt zu einem TypeError): Die von ihr erzeugten Generatorobjekte sind Instanzen davon, Methoden, die zu g.prototype hinzugefügt werden, werden zu Prototyp-Methoden usw.

> function* g() {}
> g.prototype.hello = function () { return 'hi!'};
> const obj = g();
> obj instanceof g
true
> obj.hello()
'hi!'

Zweitens, wenn Sie Methoden für alle Generatorobjekte verfügbar machen möchten, fügen Sie sie am besten zu (Generator).prototype hinzu. Ein Weg, auf dieses Objekt zuzugreifen, ist folgender:

const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.hello = function () { return 'hi!'};
const generatorObject = (function* () {})();
generatorObject.hello(); // 'hi!'

22.7.1 IteratorPrototype

Im Diagramm gibt es kein (Iterator), da ein solches Objekt nicht existiert. Aber angesichts der Funktionsweise von instanceof und da (IteratorPrototype) ein Prototyp von g1() ist, könnten Sie immer noch sagen, dass g1() eine Instanz von Iterator ist.

Alle Iteratoren in ES6 haben (IteratorPrototype) in ihrer Prototypenkette. Dieses Objekt ist iterierbar, da es die folgende Methode hat. Daher sind alle ES6-Iteratoren iterierbar (als Konsequenz davon können Sie for-of usw. darauf anwenden).

[Symbol.iterator]() {
    return this;
}

Die Spezifikation empfiehlt die Verwendung des folgenden Codes, um auf (IteratorPrototype) zuzugreifen:

const proto = Object.getPrototypeOf.bind(Object);
const IteratorPrototype = proto(proto([][Symbol.iterator]()));

Sie könnten auch verwenden:

const IteratorPrototype = proto(proto(function* () {}.prototype));

Zitat aus der ECMAScript 6 Spezifikation:

ECMAScript-Code kann auch Objekte definieren, die von IteratorPrototype erben. Das Objekt IteratorPrototype bietet einen Ort, an dem zusätzliche Methoden hinzugefügt werden können, die für alle Iteratorobjekte gelten.

IteratorPrototype wird wahrscheinlich in einer zukünftigen Version von ECMAScript direkt zugänglich sein und Tool-Methoden wie map() und filter() enthalten (Quelle).

22.7.2 Der Wert von this in Generatoren

Eine Generatorfunktion kombiniert zwei Anliegen

  1. Sie ist eine Funktion, die ein Generatorobjekt einrichtet und zurückgibt.
  2. Sie enthält den Code, den das Generatorobjekt Schritt für Schritt durchläuft.

Deshalb ist es nicht sofort ersichtlich, was der Wert von this innerhalb eines Generators sein sollte.

Bei Funktionsaufrufen und Methodenaufrufen ist this das, was es wäre, wenn gen() keine Generatorfunktion, sondern eine normale Funktion wäre.

function* gen() {
    'use strict'; // just in case
    yield this;
}

// Retrieve the yielded value via destructuring
const [functionThis] = gen();
console.log(functionThis); // undefined

const obj = { method: gen };
const [methodThis] = obj.method();
console.log(methodThis === obj); // true

Wenn Sie this in einem Generator zugreifen, der über new aufgerufen wurde, erhalten Sie einen ReferenceError (Quelle: ES6 spec)

function* gen() {
    console.log(this); // ReferenceError
}
new gen();

Eine Umgehung besteht darin, den Generator in eine normale Funktion zu wickeln, die dem Generator sein Generatorobjekt über next() übergibt. Das bedeutet, dass der Generator seinen ersten yield verwenden muss, um sein Generatorobjekt abzurufen.

const generatorObject = yield;

22.8 Stilistische Überlegung: Leerzeichen vor und nach dem Sternchen

Angemessene – und legale – Variationen der Formatierung des Sternchens sind:

Lassen Sie uns herausfinden, welche dieser Variationen für welche Konstrukte sinnvoll sind und warum.

22.8.1 Generator-Funktionsdeklarationen und -ausdrücke

Hier wird das Sternchen nur verwendet, weil generator (oder etwas Ähnliches) nicht als Schlüsselwort verfügbar ist. Wenn es das wäre, würde eine Generatorfunktionsdeklaration so aussehen:

generator foo(x, y) {
    ···
}

Anstelle von generator markiert ECMAScript 6 das Schlüsselwort function mit einem Sternchen. Somit kann function* als Synonym für generator betrachtet werden, was vorschlägt, Generatorfunktionsdeklarationen wie folgt zu schreiben.

function* foo(x, y) {
    ···
}

Anonyme Generatorfunktionsausdrücke würden so formatiert:

const foo = function* (x, y) {
    ···
}

22.8.2 Generator-Methodendefinitionen

Beim Schreiben von Generator-Methodendefinitionen empfehle ich, das Sternchen wie folgt zu formatieren.

const obj = {
    * generatorMethod(x, y) {
        ···
    }
};

Es gibt drei Argumente dafür, ein Leerzeichen nach dem Sternchen zu schreiben.

Erstens sollte das Sternchen nicht Teil des Methodennamens sein. Einerseits ist es nicht Teil des Namens einer Generatorfunktion. Andererseits wird das Sternchen nur bei der Definition eines Generators erwähnt, nicht bei seiner Verwendung.

Zweitens ist eine Generator-Methodendefinition eine Abkürzung für die folgende Syntax. (Um meinen Punkt zu machen, gebe ich dem Funktionsausdruck redundant auch einen Namen.)

const obj = {
    generatorMethod: function* generatorMethod(x, y) {
        ···
    }
};

Wenn bei Methodendefinitionen das Schlüsselwort function weggelassen wird, sollte dem Sternchen ein Leerzeichen folgen.

Drittens sind Generator-Methodendefinitionen syntaktisch ähnlich zu Gettern und Settern (die bereits in ECMAScript 5 verfügbar sind).

const obj = {
    get foo() {
        ···
    }
    set foo(value) {
        ···
    }
};

Die Schlüsselwörter get und set können als Modifikatoren einer normalen Methodendefinition betrachtet werden. Wohl ist ein Sternchen auch ein solcher Modifikator.

22.8.3 Formatierung des rekursiven yield

Das Folgende ist ein Beispiel für eine Generatorfunktion, die ihre eigenen ausgegebenen Werte rekursiv ausgibt.

function* foo(x) {
    ···
    yield* foo(x - 1);
    ···
}

Das Sternchen kennzeichnet eine andere Art von yield-Operator, weshalb die obige Schreibweise sinnvoll ist.

22.8.4 Dokumentation von Generatorfunktionen und -methoden

Kyle Simpson (@getify) schlug etwas Interessantes vor: Da wir oft Klammern anhängen, wenn wir über Funktionen und Methoden wie Math.max() sprechen, wäre es dann nicht sinnvoll, ein Sternchen voranzustellen, wenn wir über Generatorfunktionen und -methoden sprechen? Sollten wir zum Beispiel *foo() schreiben, um uns auf die Generatorfunktion im vorherigen Unterabschnitt zu beziehen? Lassen Sie mich dagegen argumentieren.

Wenn es darum geht, eine Funktion zu schreiben, die ein Iterable zurückgibt, ist ein Generator nur eine von mehreren Optionen. Ich denke, es ist besser, dieses Implementierungsdetail nicht über markierte Funktionsnamen preiszugeben.

Außerdem verwendet man das Sternchen nicht beim Aufrufen einer Generatorfunktion, wohl aber Klammern.

Schließlich liefert das Sternchen keine nützliche Information – yield* kann auch mit Funktionen verwendet werden, die ein Iterable zurückgeben. Aber es kann sinnvoll sein, die Namen von Funktionen und Methoden (einschließlich Generatoren), die Iterables zurückgeben, zu markieren. Zum Beispiel über den Suffix Iter.

22.9 FAQ: Generatoren

22.9.1 Warum das Schlüsselwort function* für Generatoren und nicht generator?

Aufgrund der Abwärtskompatibilität war die Verwendung des Schlüsselworts generator keine Option. Zum Beispiel könnte der folgende Code (ein hypothetischer ES6 anonymer Generator-Ausdruck) ein ES5 Funktionsaufruf gefolgt von einem Codeblock sein.

generator (a, b, c) {
    ···
}

Ich finde, dass sich das Sternchen-Namensschema gut auf yield* überträgt.

22.9.2 Ist yield ein Schlüsselwort?

yield ist nur im Strict Mode ein reserviertes Wort. Ein Trick wird verwendet, um es in den ES6 Sloppy Mode zu bringen: Es wird zu einem kontextuellen Schlüsselwort, das nur innerhalb von Generatoren verfügbar ist.

22.10 Fazit

Ich hoffe, dieses Kapitel hat Sie davon überzeugt, dass Generatoren ein nützliches und vielseitiges Werkzeug sind.

Ich mag, dass Generatoren es Ihnen ermöglichen, kooperativ gemanagte Aufgaben zu implementieren, die beim Aufrufen asynchroner Funktionen blockieren. Meiner Meinung nach ist das das richtige mentale Modell für asynchrone Aufrufe. Hoffentlich geht JavaScript in Zukunft weiter in diese Richtung.

22.11 Weiterführende Lektüre

Quellen dieses Kapitels

[1] „Async Generator Proposal“ von Jafar Husain

[2] „A Curious Course on Coroutines and Concurrency“ von David Beazley

[3] „Why coroutines won’t work on the web“ von David Herman

Weiter: V Standardbibliothek