Heim  >  Artikel  >  Web-Frontend  >  Parsen von Generatoren in JavaScript

Parsen von Generatoren in JavaScript

藏色散人
藏色散人nach vorne
2021-09-21 17:16:241861Durchsuche

Um das Funktionsprinzip von Redux Sagas besser zu verstehen, habe ich vor Kurzem das Wissen über JavaScript-Generatoren in einem Artikel zusammengefasst Der Artikel ist sowohl leicht verständlich als auch streng genug, um als Anfängerleitfaden für die Verwendung von Generatoren zu dienen.

Einführung

JavaScript führte Generatoren in ES6 ein. Generatorfunktionen ähneln regulären Funktionen, können jedoch angehalten und fortgesetzt werden. Generatoren sind auch eng mit Iteratoren verbunden, da Generatorobjekte Iteratoren sind.
In JavaScript können Sie nach einem Funktionsaufruf im Allgemeinen nicht pausieren oder stoppen. (Ja, asynchrone Funktionen halten an, während sie auf eine Wait-Anweisung warten, aber asynchrone Funktionen wurden erst in ES7 eingeführt. Außerdem werden asynchrone Funktionen auf Generatoren aufgebaut.) Eine normale Funktion endet nur, wenn sie einen Fehler zurückgibt oder auslöst.

function foo() {
  console.log('Starting');
  const x = 42;
  console.log(x);  
  console.log('Stop me if you can');  
  console.log('But you cannot');
 }

Im Gegensatz dazu ermöglichen uns Generatoren, die Ausführung an einem beliebigen Haltepunkt anzuhalten und die Ausführung am selben Haltepunkt fortzusetzen.

Generatoren und Iteratoren

Von MDN:

In JavaScript ist ein Iterator ein Objekt, das eine Sequenz definiert und bei Beendigung einen Rückgabewert zurückgeben kann. >Genauer gesagt ist ein Iterator jedes Objekt, das das Iterator-Protokoll implementiert>durch die Verwendung der next()-Methode, die ein Objekt mit zwei Eigenschaften zurückgibt: value, der der nächste Wert in der Sequenz ist; und done, wenn er wahr ist wenn der letzte Wert in der Sequenz iteriert wurde. Wenn value und done zusammen vorhanden sind, ist dies der Rückgabewert des Iterators.

Das Wesen eines Iterators ist also:

  • ein Objekt, das eine Sequenz definiert
  • hat eine next()Methode ...
  • gibt ein Objekt mit zwei Eigenschaften zurück: Wert und fertig

Zum Erstellen wird ein Generator benötigt ein Iterator? NEIN. Tatsächlich können wir mit Abschlüssen vor ES6 bereits eine unendliche Fibonacci-Sequenz erstellen, wie im folgenden Beispiel gezeigt:

var fibonacci = {
  next: (function () {
    var pre = 0, cur = 1;
    return function () {
      tmp = pre;
      pre = cur;
      cur += tmp;
      return cur;
    };
  })()
};

fibonacci.next(); // 1
fibonacci.next(); // 2
fibonacci.next(); // 3
fibonacci.next(); // 5
fibonacci.next(); // 8

Bezüglich der Vorteile von Generatoren zitiere ich noch einmal MDN:

Benutzerdefinierte Iteratoren sind zwar ein nützliches Werkzeug, aber Ihre Erstellung erfordert eine sorgfältige Programmierung unsererseits, da ihr interner Zustand explizit beibehalten werden muss. Generatorfunktionen bieten eine leistungsstarke Alternative: Sie ermöglichen es uns, iterative Algorithmen zu definieren, indem wir eine Funktion schreiben, deren Ausführung nicht kontinuierlich ist.
Mit anderen Worten: Die Verwendung von Generatoren zum Erstellen von Iteratoren ist einfacher (keine Schließungen erforderlich!), was eine geringere Fehlerwahrscheinlichkeit bedeutet.
Die Beziehung zwischen Generatoren und Iteratoren besteht darin, dass das von der Generatorfunktion zurückgegebene Generatorobjekt ein Iterator ist.

Syntax

Generatorfunktionen werden mit der Function*-Syntax erstellt und mit dem Schlüsselwort yield angehalten.
Der anfängliche Aufruf einer Generatorfunktion führt keinen ihrer Codes aus, sondern gibt ein Generatorobjekt zurück. Dieser Wert wird durch Aufrufen der next()-Methode des Generators verwendet, die den Code ausführt, bis das Schlüsselwort yield gefunden wird, und dann pausiert, bis next() erneut aufgerufen wird.

function * makeGen() {
  yield 'Hello';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'World', done: false }
g.next(); // { value: undefined, done: true }

Wiederholte Aufrufe von g.next() nach der letzten Anweisung oben geben einfach dasselbe Rückgabeobjekt zurück (oder genauer gesagt, erzeugen es): { value: undefiniert, erledigt: true }.

Yield unterbricht die Ausführung

Möglicherweise bemerken Sie etwas Besonderes an dem obigen Codeausschnitt. Der zweite Aufruf von next() erzeugt ein Objekt mit der Eigenschaft done: false statt done: true.
Da wir die letzte Anweisung in der Generatorfunktion ausführen, sollte das done-Attribut dann nicht wahr sein? nicht wirklich. Wenn eine yield-Anweisung auftritt, wird der darauf folgende Wert (in diesem Fall „World“) generiert und die Ausführung angehalten. Daher pausiert der zweite Aufruf von next() bei der zweiten Yield-Anweisung, sodass die Ausführung noch nicht abgeschlossen ist. Die Ausführung ist erst dann abgeschlossen, wenn die Ausführung nach der zweiten Yield-Anweisung fortgesetzt wird (d. h. done: true) und der Code nicht erneut ausgeführt wird.
Wir können uns den next()-Aufruf so vorstellen, dass er das Programm anweist, zur nächsten yield-Anweisung (vorausgesetzt, sie existiert) zu laufen, einen Wert zu generieren und zu pausieren. Das Programm wird nicht wissen, dass nach der yield-Anweisung nichts steht, bis es die Ausführung fortsetzt, und die Ausführung kann nur mit einem weiteren next()-Aufruf fortgesetzt werden.

Yield und Return

Im obigen Beispiel verwenden wir Yield, um den Wert außerhalb des Generators zu übergeben. Wir können auch return verwenden (genau wie in einer normalen Funktion); allerdings beendet die Verwendung von return die Ausführung und setzt done: true.

function * makeGen() {
  yield 'Hello';
  return 'Bye';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'Bye', done: true }
g.next(); // { value: undefined, done: true }

Da die Ausführung bei der Return-Anweisung nicht pausiert und per Definition kein Code nach der Return-Anweisung ausgeführt werden kann, wird done auf true gesetzt.

yield: Argument für die nächste Methode

Bisher haben wir yield verwendet, um einen Wert außerhalb des Generators zu übergeben (und seine Ausführung anzuhalten).
Yield ist jedoch tatsächlich bidirektional und ermöglicht uns die Übergabe von Werten an Generatorfunktionen.

function * makeGen() {
  const foo = yield 'Hello world';
  console.log(foo);
}

const g = makeGen();
g.next(1); // { value: 'Hello world', done: false }
g.next(2); // logs 2, yields { value: undefined, done: true }

等一下。不应该是"1"打印到控制台,但是控制台打印的是"2"?起初,我发现这部分在概念上与直觉相反,因为我预期的赋值foo = 1。毕竟,我们将“1”传递到next()方法调用中,从而生成Hello world,对吗?  
但事实并非如此。传递给第一个next(...)调用的值将被丢弃。除了这似乎是ES6规范之外,实际上没有其他原因.从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
我喜欢这样对程序的执行进行合理化:

  • 在第一个next()调用时,它将一直运行,直到遇到yield 'Hello world',在此基础上生成{ value: 'Hello world', done: false }和暂停。就是这么回事。正如大家所看到的,传递给第一个next()调用的任何值都是不会被使用的(因此被丢弃)。
  • 当再次调用next(...)时,执行将恢复。在这种情况下,执行需要为常量foo分配一些值(由yield语句决定)。因此,我们对next(2)的第二次调用赋值foo=2。程序不会在这里停止—它会一直运行,直到遇到下一个yield或return语句。在本例中,没有更多的yield,因此它记录2并返回undefined的done: true。在生成器使用异步因为yield是一个双向通道,允许信息在两个方向上流动,所以它允许我们以非常酷的方式使用生成器。到目前为止,我们主要使用yield在生成器之外传递值。但是我们也可以利用yield的双向特性以同步方式编写异步函数。

使用上面的概念,我们可以创建一个类似于同步代码但实际上执行异步函数的基本函数:

function request(url) {
  fetch(url).then(res => {
    it.next(res); // Resume iterator execution
  });
}

function * main() {
  const rawResponse = yield request('https://some-url.com');
  const returnValue = synchronouslyProcess(rawResponse);
  console.log(returnValue);
}

const it = main();
it.next(); // Remember, the first next() call doesn't accept input

这是它的工作原理。首先,我们声明一个request函数和main生成器函数。接下来,通过调用main()创建一个迭代器it。然后,我们从调用it.next()开始。  
在第一行的function * main(),在yield request('https://some-url.com')之后执行暂停。request()隐式地返回undefined,因此我们实际上生成了undefined值,但这并不重要—我们没有使用该值。  
当request()函数中的fetch()调用完成时,it.next(res)将会被调用并完成下列两件事:
it继续执行;和  
it将res传递给生成器函数,该函数被分配给rawResponse  
最后,main()的其余部分将同步完成。  
这是一个非常基础的设置,应该与promise有一些相似之处。有关yield和异步性的更详细介绍,请参阅此文。

生成器是一次性

我们不能重复使用生成器,但可以从生成器函数创建新的生成器。

function * makeGen() {
  yield 42;
}

const g1 = makeGen();
const g2 = makeGen();
g1.next(); // { value: 42, done: false }
g1.next(); // { value: undefined, done: true }
g1.next(); // No way to reset this!
g2.next(); // { value: 42, done: false }
...
const g3 = makeGen(); // Create a new generator
g3.next(); // { value: 42, done: false }

无限序列

迭代器表示序列,有点像数组。所以,我们应该能够将所有迭代器表示为数组,对吧?
然而,并不是的。数组在创建时需要立即分配,而迭代器是延迟使用的。数组是迫切需要的,因为创建一个包含n个元素的数组需要首先创建/计算所有n个元素,以便将它们存储在数组中。相反,迭代器是惰性的,因为序列中的下一个值只有在使用时才会创建/计算。  
因此,表示无限序列的数组在物理上是不可能的(我们需要无限内存来存储无限项!),而迭代器可以轻松地表示(而不是存储)该序列。  
让我们创建一个从1到正无穷数的无穷序列。与数组不同,这并不需要无限内存,因为序列中的每个值只有在使用时才会懒散地计算出来。

function * makeInfiniteSequence() {
  var curr = 1;
  while (true) {
    yield curr;
    curr += 1;
  }
}

const is = makeInfiniteSequence();
is.next(); { value: 1, done: false }
is.next(); { value: 2, done: false }
is.next(); { value: 3, done: false }
... // It will never end

有趣的事实:这类似于Python生成器表达式vs列表理解。虽然这两个表达式在功能上是相同的,但是生成器表达式提供了内存优势,因为值的计算是延迟的,而列表理解则是立即计算值并创建整个列表。

推荐学习:《javascript基础教程

Das obige ist der detaillierte Inhalt vonParsen von Generatoren in JavaScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen