最近、Redux Sagas がどのように機能するかをよりよく理解するために、JavaScript ジェネレーターの知識を再学習しました。インターネットからいくつかの情報を収集しました。 . さまざまな知識を1つの記事に凝縮しましたので、わかりやすく、それでいてしっかりとした内容で、初心者がジェネレーターを使う際のガイドとして活用していただければ幸いです。
はじめに
JavaScript は ES6 でジェネレーターを導入しました。ジェネレーター関数は通常の関数と似ていますが、一時停止および再開できる点が異なります。ジェネレーター オブジェクトはイテレーターであるため、ジェネレーターはイテレーターとも密接に関係しています。
JavaScript では、通常、関数呼び出し後に一時停止または停止することはできません。 (はい、async 関数は await ステートメントの待機中に一時停止しますが、async 関数は ES7 でのみ導入されました。また、async 関数はジェネレーターの上に構築されています。) 通常の関数は、エラーを返すかスローした場合にのみ終了します。
function foo() { console.log('Starting'); const x = 42; console.log(x); console.log('Stop me if you can'); console.log('But you cannot'); }
対照的に、ジェネレーターを使用すると、任意のブレークポイントで実行を一時停止し、同じブレークポイントから実行を再開できます。
ジェネレータとイテレータ
MDN より:
JavaScript では、イテレータはシーケンスを定義するオブジェクトであり、終了時に戻り値を返す場合があります。 >より具体的には、イテレータとは、next() メソッドを使用して Iterator プロトコルを実装する任意のオブジェクトです。このメソッドは、シーケンス内の次の値である value と true の場合は Done という 2 つのプロパティを持つオブジェクトを返します。シーケンス内の最後の値が反復された場合。 valueとdoneが同時に存在する場合、それがイテレータの戻り値となります。
したがって、イテレータの本質は次のとおりです。
- シーケンスを定義するオブジェクトには next()
- メソッドがあります。 .
2 つのプロパティを持つオブジェクトを返します: value と doned
- 反復子を作成するにはジェネレーターが必要ですか?いいえ。実際、次の例に示すように、ES6 以前のクロージャを使用して無限のフィボナッチ数列を作成できます:
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
ジェネレーターの利点については、MDN を再度引用します:
にもかかわらずカスタム イテレータは便利なツールですが、内部状態を明示的に維持する必要があるため、カスタム イテレータを作成するには慎重なプログラミングが必要です。ジェネレーター関数は強力な代替手段を提供します。ジェネレーター関数を使用すると、実行が連続的ではない関数を作成することで反復アルゴリズムを定義できます。
言い換えると、ジェネレーターを使用してイテレーターを作成する方が簡単です (クロージャーは必要ありません!)。つまり、エラーが発生する可能性が低くなります。ジェネレータとイテレータの関係は、ジェネレータ関数によって返されるジェネレータ オブジェクトがイテレータであるということです。
構文
ジェネレーター関数は function* 構文を使用して作成され、yield キーワードを使用して一時停止されます。
最初にジェネレーター関数を呼び出しても、そのコードは実行されず、代わりにジェネレーター オブジェクトが返されます。この値は、ジェネレーターの next() メソッドを呼び出すことによって使用されます。このメソッドは、yield キーワードが見つかるまでコードを実行し、次に next() が再度呼び出されるまで一時停止します。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 }
上記の最後のステートメントの後に g.next() を繰り返し呼び出すと、同じ戻りオブジェクト: { value: unfined,ned: true } のみが返されます (より正確には、生成されます)。
yield は実行を一時停止します
上記のコード スニペットには特別な点があることに気づくかもしれません。 2 番目の next() 呼び出しでは、done: true ではなく、done: false プロパティを持つオブジェクトが生成されます。
ジェネレーター関数の最後のステートメントを実行しているので、done 属性は true であるべきではないでしょうか?あまり。 yield ステートメントが見つかると、それに続く値 (この場合は「World」) が生成され、実行が一時停止されます。したがって、2 番目の next() 呼び出しは 2 番目の yield ステートメントで一時停止するため、実行はまだ完了していません。2 番目の yield ステートメントの後に実行が再開される (つまり、done: true) まで実行は完了せず、コードを再度実行することはありません。next() 呼び出しは、次の yield ステートメント (存在すると仮定して) まで実行し、値を生成して一時停止するようにプログラムに指示すると考えることができます。プログラムは、実行を再開するまで、yield ステートメントの後に何もないことを知りません。実行は、別の next() 呼び出しでのみ再開できます。
yield と return
上記の例では、yield を使用してジェネレーターの外部に値を渡します。 (通常の関数と同様に) return を使用することもできますが、return を使用すると実行が終了し、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 }
return ステートメントでは実行が一時停止せず、定義上、return ステートメントの後にコードを実行できないため、done は true に設定されます。
yield: 次のメソッドのパラメータ
これまでは、yield を使用してジェネレーターの外に値を渡してきました (そして、その実行を一時停止しました)。
ただし、yield は実際には双方向であり、ジェネレーター関数に値を渡すことができます。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基础教程》