thelonglqd thelonglqd - 1 month ago 20
Javascript Question

Work flow of a JavaScript generator function

I read these line of codes from an article and they showed the work flow of function generator.

var foo, f;

foo = function* () {
console.log('generator 1');
console.log('yield 1', yield 'A');
console.log('generator 2');
console.log('yield 2', yield 'B');
console.log('generator 3');
};

f = foo();

console.log('tick 1');
console.log(f.next('a'));
console.log('tick 2');
console.log(f.next('b'));
console.log('tick 3');
console.log(f.next('c'));
console.log('tick 4');
console.log(f.next('d'));


This is the log in the terminal:

tick 1
generator 1
{ value: 'A', done: false }
tick 2
yield 1 b
generator 2
{ value: 'B', done: false }
tick 3
yield 2 c
generator 3
{ value: undefined, done: true }
tick 4
{ value: undefined, done: true }


But I can not find the way to easily understand this flow, it is kind of weird. If someone has a simple approach to this problem, please help to explain.

Answer

Basics

Calling the generator function returns an iterator.

Calling .next() on an iterator returns an object in the form:

{ 
  value // current value of the iterator, 
  done  // boolean indicating if iteration is finished 
}

Calling .next() on the iterator provided by the generator, runs the code from the current paused point in the generator to the next yield, pauses the generator on this next yield and pushes out whatever is the yield value as the value returned from the .next() method of the iterator, in the object form shown above.

Anything that you pass into .next() of this iterator will be returned from the currently paused yield within the generator.

Since the generator isn't paused on a yield in the first call to .next(), anything passed into the first .next() is is just ignored.

If there are no yield statements left, whatever the function returns will be the last iterator value.

At this point the done flag will be set to true and any further calls to .next() will return the same value.


Running Example

So, in terms of your code, here's what is happening. I'll comment out each line that happens after an execution step.

Step 1

f = foo();

At this point, the iterator is created and stored in f but no code in the generator has actually ran. So we have:

function* () {
  console.log('generator 1');
  console.log('yield 1', yield 'A');
  console.log('generator 2');
  console.log('yield 2', yield 'B');
  console.log('generator 3');
};

Step 2

f.next('a'); // returns { value: 'A', done: false }

This runs the code within the generator up to the first yield and pushes the yielded 'A' out of the .next() call. The 'a' that is passed into .next() is just ignored, since its the first call (explained above).

Commenting out the lines that ran leaves us with:

function* () {
  // console.log('generator 1');
  console.log('yield 1', PAUSE_POINT); // we're paused on the `yield`. Essentially half of this line is done
  console.log('generator 2');
  console.log('yield 2', yield 'B');
  console.log('generator 3');
};

Step 3

f.next('b'); // return { value: 'B', done: false }

This returns 'b' from the first yield (the first PAUSE_POINT) and runs the code to the next yield, pushing 'B' out from the iterator.

Removing the lines that run leaves with:

function* () {
  // console.log('generator 1');
  // console.log('yield 1', 'b'); // this PAUSE_POINT returns 'b'
  // console.log('generator 2');
  console.log('yield 2', PAUSE_POINT); // no we're paused here
  console.log('generator 3');
};

Step 4

f.next('c'); // { value: undefined, done: true }

Which passes 'c' out of the paused yield and, since there are no more yields left, runs to the end of the generator and pushes out whatever the generator returns, which in your case is just an implicit return undefined. Since we reached the end of the generator function, the done flag is set to true.

function* () {
  // console.log('generator 1');
  // console.log('yield 1', 'b'); // this PAUSE_POINT returns 'b'
  // console.log('generator 2');
  // console.log('yield 2', 'c'); // this PAUSE_POINT returns 'c'
  // console.log('generator 3');
  // here we have an implicit return undefined;
};

Step 5 and further on

f.next('d'); // { value: undefined, done: true }

Any calls to .next() after the generator is finished (i.e. done is true) just returns whatever the last value was. Passing 'd' into this method serves no purpose anymore.


Additional Example

var foo = function* () {
  console.log('first call to next runs to yield #1');
  var a = yield 'A'; // this yield pushes 'A' and returns 'a'
  console.log('second call to next runs to yield #2');
  var b = yield 'B'; // this yield pushes 'B' and returns 'b'
  console.log('third call to next runs to the end of the generator ');
  // there's no return statement here so we are returning undefined
};

var f = foo();

console.log(f.next('this gets ignored')); // { value: 'A', done: false }
console.log(f.next('a'));                 // { value: 'B', done: false }
console.log(f.next('b'));                 // { value: undefined, done: true }

// any further call just returns whatever the last returned value was
console.log(f.next('this also gets ignored since we are done')); // { value: undefined, done: true }

Comments