Generator Functions in JS

Generator Functions in JS

What are Generator Functions?

Generator functions are a special type of functions in JavaScript, introduced in ES6, that have the built-in capability to be paused and resumed allowing us to take control of the execution flow and generate multiple values.

The syntax, as shown below, is pretty much similar to regular functions apart from the new function* keyword.

function* someGeneratorFunction() {
  // function code goes here
}        

But what does it mean to “pause and resume” execution?

To understand this better, let’s look at how regular functions behave.

Regular functions execute from start to finish sequentially. Once a function is invoked, it continues execution until it encounters either a return statement or reaches the end of the function body.

A basic demonstration:

function someRegularFunction() {
  console.log("1");
  console.log("2");
  console.log("3");
}

someRegularFunction(); 

// Output will be as follows:
// 1
// 2
// 3        

In the snippet above, when the function is called, it executes each line linearly (by the order they were defined) and it is reflected in the output.

Generator Functions on the other hand can use the yield keyword to pause execution and generate a value. When this value is yielded, the state of the function is saved and can be resumed at a later time, by calling the next() method, from where it left off.

This means that we can yield multiple values by exiting and re-entering the function.

A basic demonstration:

Note: Outputs of the calls are commented below each line

function* someGeneratorFunction() {
  console.log("Start of the function");
  yield 1;
  console.log("Middle of the function");
  yield 2;
  console.log("End of the function");
}

const generator = someGeneratorFunction(); // Returns generator object


console.log(generator.next().value); 
// Start of the function
// 1

console.log(generator.next().value);
// Middle of the function
// 2

console.log(generator.next().value);
// End of the function
// undefined        

In the snippet above, calling the generator function someGeneratorFunction* returns a generator object. On this object, we can call the next() method, which will cause the generator function to execute. If a yield is encountered, the method returns an object that contains a value property containing the yielded value and a boolean done property which indicates if the generator has yielded its last value or not. To demonstrate this, we will log the entire object returned from next() (as opposed to the value property as we did in the previous example).

function* someGeneratorFunction() {
  yield 1;
}

const generator = someGeneratorFunction();

console.log(generator.next()); // {value: 1, done: false}
console.log(generator.next()); // {value: undefined, done: true}        

What happens if we add a return statement in a generator function?

Much like how it behaves in regular functions, a return statement will cause the generator function to finish executing, making the subsequent lines of codes unreachable. It does this by setting the done property to true.

function* yieldAndReturn() {
  yield 1;
  return "Returned";
  yield "Unreachable";
}

const generator = yieldAndReturn();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: "Returned", done: true }
console.log(generator.next()); // { value: undefined, done: true }        

Note: In a real world implementation, we wouldn’t know how many times to request for the values, so we would need to keep requesting values in a loop until we receive the done: true property.

Why and When do we use Generator Functions?

You might have been wondering — couldn’t this “pause and resume” functionality be achieved using regular functions?

Well, sure it can:

  • Using Callbacks: Consider you need to make multiple API requests where each call depends on the result of the previous call. Slowly but surely you will find yourself in a callback hell. This happens when the number of calls you need to make increases, making the code complex and hard to understand.

Here is a painful example:

asyncFunction1(arg1, (err, result1) => {
  if (err) {
    console.error(err);
  } else {
    asyncFunction2(result1, (err, result2) => {
      if (err) {
        console.error(err);
      } else {
        asyncFunction3(result2, (err, result3) => {
          if (err) {
            console.error(err);
          } else {
            // Do something with the final result
          }
        });
      }
    });
  }
});        

  • Promises: Continuing with our previous multiple API call scenario, we might think of using a Promise. then() will solve the issue, but it is a trap. Even though it is more readable than the callback hell, we will only end up with complex promise chains that can be hard to understand & debug:

asyncFunction1(arg1)
  .then(result1 => asyncFunction2(result1))
  .then(result2 => asyncFunction3(result2))
  .then(result3 => {
    // Do something with the final result
  })
  .catch(err => {
    console.error(err);
  });        

Why use Generator Functions?

  • Readability: Generator functions provide a simple-to-understand syntax. The yield denotes the parts of the function where it pauses and resumes, making it easier to read & and maintain our code.
  • Stateful Iteration: Generator functions create iterator objects which can be used to re-enter the function and generate multiple values. They maintain an internal state between successive yield statements, which makes it easier to carry out computations across iterations.
  • Lazy Evaluation: The values of a generator function are yielded on demand as opposed to being returned all at once. This helps with efficient memory usage as we don’t need to store all values to memory upfront.

When to use Generator Functions:

Let’s see a few scenarios where using a generator function might come in handy.

  1. Iteration and Sequences: Useful when working with infinite sets without needing to provide the values all at once, such as data streams or even a simple Fibonacci sequence :)

function* fibonacciSequence() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fibonacciGenerator = fibonacciSequence();
console.log(fibonacciGenerator.next().value); // Output: 0
console.log(fibonacciGenerator.next().value); // Output: 1
console.log(fibonacciGenerator.next().value); // Output: 1
console.log(fibonacciGenerator.next().value); // Output: 2        

2. Asynchronous Programming

Generator functions handle asynchronous code with ease by avoiding complex promise chains. They are the foundations on which async/await syntax is built.

Since yield stops the execution of the function, we can use it to wait for an asynchronous request to resolve.

Solving the previous callback hell

Remember our previous callback example where we encountered the callback hell? Recalling that we needed to make multiple API calls where the current call depends on the result of the previous one, here is how generator functions solve it:

function* asyncFunction1(arg1) {
  try {
    const result1 = yield async_function1(arg1);
    const result2 = yield async_function2(result1);
    const result3 = yield async_function3(result2);
    // Do something with the final result
  } catch (err) {
    console.error(err);
  }
}

function run(generator) {
  const iterator = generator(); // Create the generator object

  function iterate({ value, done }) { // Destructure the value & done properties
    if (done) {
      return;
    }

    value
      .then((result) => iterate(iterator.next(result))) // Call the next iteration and pass the current result
      .catch((err) => iterate(iterator.throw(err)));
  }

  iterate(iterator.next()); // Start the iteration
}

run(asyncFunction1);        

This code can easily be rewritten using async/await — Even better!

Note that async/await uses generator functions behind the scenes.

async function asyncFunction1(arg1) {
  try {
    const result1 = await asyncFunction1(arg1);
    const result2 = await asyncFunction2(result1);
    const result3 = await asyncFunction3(result2);
    // Do something with the final result
  } catch (err) {
    console.error(err);
  }
}

asyncFunction1();        

Conclusion

As we have seen, generator functions allow you to define functions that can be paused and resumed, enabling the creation of iterators and asynchronous programming constructs like async/await. They provide an easy-to-understand syntax, as well as an optimized method of handling iterations for yielding multiple values on demand. These features make them particularly useful in scenarios that involve working with sequences, implementing iterators, and writing asynchronous code.

Best Practices

ES6

JavaScript

Programming

(source = "internet")

要查看或添加评论,请登录

社区洞察

其他会员也浏览了