최근 Redux Sagas의 작동 원리를 더 잘 이해하기 위해 JavaScript Generator에 대한 지식을 다시 배웠고, 인터넷에서 수집한 다양한 지식 포인트를 기사로 압축했습니다. 이 기사는 이해하기 쉽고 생성기 사용에 대한 초보자 가이드 역할을 할 만큼 엄격합니다.
소개
JavaScript는 ES6에 생성기를 도입했습니다. 생성기 함수는 일시 중지하고 다시 시작할 수 있다는 점을 제외하면 일반 함수와 같습니다. Generator 객체는 Iterator이기 때문에 Generator는 Iterator와도 밀접한 관련이 있습니다.
JavaScript에서는 일반적으로 함수 호출 후에 일시 중지하거나 중지할 수 없습니다. (예, 비동기 함수는 wait 문을 기다리는 동안 일시 중지되지만 비동기 함수는 ES7에서만 도입되었습니다. 또한 비동기 함수는 생성기 위에 구축됩니다.) 일반 함수는 오류를 반환하거나 던질 때만 종료됩니다.
function foo() { console.log('Starting'); const x = 42; console.log(x); console.log('Stop me if you can'); console.log('But you cannot'); }
반대로 생성기를 사용하면 임의의 중단점에서 실행을 일시 중지하고 동일한 중단점에서 실행을 다시 시작할 수 있습니다.
Generators and Iterators
MDN에서:
JavaScript에서 반복자는 시퀀스를 정의하고 종료 시 반환 값을 반환할 수 있는 개체입니다. 보다 구체적으로 말하면, 반복자는 두 가지 속성을 가진 객체를 반환하는 next() 메서드를 사용하여 Iterator 프로토콜을 구현하는 모든 객체입니다. value는 시퀀스의 다음 값이고 true인 경우입니다. 시퀀스의 마지막 값이 반복된 경우. value와 done이 함께 존재하면 반복자의 반환 값입니다.
따라서 반복자의 본질은 다음과 같습니다.
- 시퀀스를 정의하는 객체
- 에는
next()
메서드가 있습니다... - value와 done이라는 두 가지 속성을 가진 객체를 반환합니다.
생성하려면 생성기가 필요합니까? 반복자? 아니요. 실제로 다음 예에서 볼 수 있듯이 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을 다시 인용하겠습니다.
사용자 정의 반복자는 유용한 도구이지만, 이를 생성하려면 내부 상태를 명시적으로 유지해야 하기 때문에 신중한 프로그래밍이 필요합니다. 생성기 함수는 강력한 대안을 제공합니다. 이를 통해 실행이 연속적이지 않은 함수를 작성하여 반복 알고리즘을 정의할 수 있습니다.
즉, 생성기를 사용하여 반복자를 만드는 것이 더 간단하며(클로저가 필요하지 않음!) 오류 가능성이 줄어듭니다.
제너레이터와 이터레이터의 관계는 제너레이터 함수에 의해 반환된 제너레이터 객체가 이터레이터라는 것입니다.
Syntax
Generator 함수는 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: undefound, done: true }가 반환됩니다(더 정확하게는 생성).
yield가 실행을 일시 중지합니다
위 코드 조각에서 특별한 점을 발견할 수 있습니다. 두 번째 next() 호출은 done: true 대신 done: false 속성을 가진 객체를 생성합니다.
생성기 함수의 마지막 문을 실행하고 있으므로 done 속성이 true여야 하지 않나요? 설마. 항복 문이 발견되면 그 뒤에 오는 값(이 경우 "World")이 생성되고 실행이 일시 중지됩니다. 따라서 두 번째 next() 호출은 두 번째 항복 문에서 일시 중지되므로 실행은 아직 완료되지 않습니다. 두 번째 항복 문(즉, done: true) 이후 실행이 재개될 때까지 실행은 완료되지 않으며 코드를 다시 실행하지 않습니다.
next() 호출은 프로그램에 다음 항복 문(존재한다고 가정)을 실행하고 값을 생성하고 일시 중지하라고 지시하는 것으로 생각할 수 있습니다. 프로그램은 실행을 재개할 때까지 항복 문 뒤에 아무것도 없다는 것을 알지 못하며, 실행은 다른 next() 호출을 통해서만 재개될 수 있습니다.
yield and 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基础教程》