ES6 introduced a number of new features, including promises and generators for handling asynchronous operations. Promise is known to solve part of the much-maligned callback hell problem. But the syntax of using the Promise chain-call when handling multiple asynchronous operations is also less elegant and intuitive. Generators take Promise one step further and allow us to describe our asynchronous processes synchronously.

Basic introduction

Generator functions are quite different from normal functions and have their own unique syntax. A simple Generator function looks like this:

function* greet() { yield 'hello' }
Copy the code

The first time a Generator function is called, the code inside the Generator function is not executed, but a Generator object is returned. As we mentioned in the previous article, a call to the next function on this Generator object initiates the execution of the logic inside the Generator function, suspends the function on the occasion of a yield statement, and returns the result after the yield keyword. Resuming Generator function execution after a pause can also be resumed by calling the next method of the Generator object, and the parameters passed to the next method are returned as the yield statement currently paused inside the Generator. This repeats until the code inside the Generator function completes execution. For example:

function* greet() {
  let result = yield 'hello'
  console.log(result)
}
let g = greet()
g.next() // {value: 'hello', done: false}
g.next(2) // Print 2, then return {value: undefined, done: true}
Copy the code

The arguments passed in the first call to the next method are not available internally to the generator, or have no practical meaning, because the generator function has not yet been executed. The first call to the Next method is used to start the generator function.

Yield grammar points

Yield can be followed by any valid JavaScript expression, and the yield statement can occur in places equivalent to where a regular assignment expression (such as a=3) can occur. For example:

b = 2 + a = 3 / / is illegal
b = 2 + (a = 3) / / legal

b = 2 + yield 3 / / is illegal
b = 2 + (yield 3) / / legal
Copy the code

The yield keyword has a low priority, and almost any expression after yield evaluates first and then yields a value to the outside world. Moreover, yield is the right associative operator, that is, yield yield 123 is equivalent to (yield (yield 123)).

About generator objects

The Generator object returned by a Generator function is an instance of a Generator function, which means that the returned Generator object inherits the methods on the Generator function prototype chain. For example:

function* g() {
  yield 1
}
g.prototype.greet = function () {
  console.log('hello')}let g1 = g()
console.log(g1 instanceof g) // true
g1.greet() // 'hello'
Copy the code

The [symbol.iterator] method that executes the generator object returns the generator object itself.

function* greet() {}
let g = greet()
console.log(g[Symbol.iterator]() === g) // true
Copy the code

The generator object also has the following two methods:

  1. The return method. Like the return method of the iterator interface, it is used when a generator function encounters an exception or aborts prematurely (as in the for… Automatically called when the of loop breaks ahead of completion), and the generator object becomes terminated and can no longer generate values. It can also be called manually to terminate the iterator. If you pass a parameter in the return call, that parameter will be used as the value property value of the object eventually returned.

If you happen to pause in the try block of a generator function and have a finally block, calling the return method does not terminate the generator immediately. Instead, it continues to execute the logic in the finally block before terminating the generator. If a finally block contains a yield statement, it means that you can continue calling the next method of the generator object to get the value until the finally block is finished executing. For example:

function* ff(){
  yield 1;
  try{ yield 2 }finally{ yield 3}}let fg = ff()
fg.next() // {value: 1, done: false}
fg.return(4) // {value: 4, done: true}
let ffg = ff()
ffg.next() // {value: 1, done: false}
ffg.next() // {value: 2, done: false}
ffg.return(4) // {value: 3, done: false}
ffg.next() // {value: 4, done: true}
Copy the code

As you can see from the above example, if the finally block is fired just after calling the return method and there is a yield statement in the finally code, the generator object will not end immediately after calling the return method. Therefore, you should not use yield statements ina finally code block in practice.

  1. Throw method. Calling this method throws an error at the point where execution of the generator function is currently paused. If the error is not caught in the generator function, the generator object state terminates and the error propagates globally from within the current throw method. When the next method is called to execute a generator function, if an error is thrown inside the generator function and not caught, it is propagated globally from within the next method.

Yield * statement

The yield* statement produces values from the iterator returned by the [symbol. iterator] method of a given Iterable. Also known as the yield delegate, this refers to delegating the current generator function to the Iterable after yield*. For this reason, yield* can be used to call another Generator function from a Generator function. For example:

function* foo() {
  yield 2
  yield 3
  return 4
}
function* bar() {
  let ret = yield* foo()
  console.log(ret) / / 4
}
Copy the code

In the example above, the return value of the propped Generator function will be the yield* return value of the propped Generator function.

Also, errors are passed between the delegated generator function and the code that controls the external generator function via yield*. For example:

function* delegated() {
  try {
    yield 1
  } catch (e) {
    console.log(e)
  }
  yield 2
  throw "err from delegate"
}

function* delegate() {
  try {
    yield* delegated()
  } catch (e) {
    console.log(e)
  }
  yield 3
}

let d = delegate()
d.next() // {value: 1, done: false}
d.throw('err')
// err
// {value: 2, done: false}
d.next()
// err from delegate
// {value: 3, done: false}
Copy the code

The last thing to note is the difference between yield* and yield. It is easy to overlook that yield* does not stop the execution of a generator function. For example:

function* foo(x) {
  if (x < 3) {
    x = yield* foo(x + 1)}return x * 2
}
let f = foo()
f.next() // {value: 24, done: true}
Copy the code

Organize asynchronous processes using generators

The basic idea of using Generator functions to handle asynchronous operations is to suspend the Generator function during an asynchronous operation, then resume the Generator function from where it was suspended by the next method of the Generator object on a callback to the completion of a phased asynchronous operation, and so on until the Generator function is finished.

It is this idea that allows a series of asynchronous operations to be written as synchronous operations within the Generator, with a more concise form. In order for the Generator functions to automate a series of internally defined asynchronous operations, additional functions are required to execute the Generator functions. For generator functions that return values other than Thunk function types each time, the CO module can be used for automatic execution. For asynchronous apis that follow callback, you need to convert them to Thunk functions and then integrate them into generator functions. For example, we have apis like this:

logAfterNs = (seconds, callback) = > 
    setTimeout((a)= > {console.log('time out'); callback()}, seconds *1000)
Copy the code

An asynchronous process looks like this:

logAfterNs(1.function(response_1) {
  logAfterNs(2.function () {... })})Copy the code

First we need to convert the asynchronous API to Thunk form, which is the original API: logAfterNs(… Args, callback), we need to modify to: thunkedLogAfterNs(… args)(callback)

function thunkify (fn) {
  return function (. args) {
    return function (callback) {
      args.push(callback)
      return fn.apply(null, args)
    }
  }
}
let thunkedLogAfterNs = thunkify(logAfterNs)
function* sequence() {
  yield thunkedLogAfterNs(1)
  yield thunkedLogAfterNs(2)}Copy the code

After converting to using generator functions to rewrite our asynchronous process, we also need a function to automatically manage and execute our generator functions.

function runTask(gen) {
  let g = gen()
  function next() {
    let result = g.next()
    if(! result.done) result.value(next) } next() } runTask(sequence)Copy the code

Better async/await

The async/await syntax introduced in ES7 is the syntactic sugar of Generator functions, except that the former no longer requires an executor. Executing an async function directly automatically executes the logic inside the function. The result of the async function execution returns a Promise object, whose state changes depending on the state of the Promise after the await statement in the async function and the final return value of the async function. Next I’ll focus on error handling in async functions.

The await keyword can be followed by a Promise object or a primitive type value. If it is a Promise object, the completion value of the Promise object is returned as the await statement. Once the Promise object is converted to the Rejected state, the Promise object returned by the async function will also be converted to the Rejected state. For example:

async function aa() {await Promise.reject('error! ')}
aa().then((a)= > console.log('resolved'), e => console.error(e)) // error!
Copy the code

If the Promise object after await is converted to Rejected, inside the async function you can call a try… Catch catches the corresponding error. For example:

async function aa() {
  try {
    await Promise.reject('error! ')}catch(e) {
    console.log(e)
  }
}
aa().then((a)= > console.log('resolved'), e => console.error(e))
// error!
// resolved
Copy the code

If the async function does not capture the Promise with the Rejected state, then capturing the aa function in the outer layer will not catch an error. Instead, the Promise object returned by the AA function will be converted to the Rejected state, as illustrated in the previous example.

In the experiment, I also tried to use a function object as the value after the await keyword, and found that await is treated as a normal value in this case, that is, the result of await expression is the function object.

async function bb(){
  let result = await ((a)= > {}); 
  console.log(result);
  return 'done'
}
bb().then(r= > console.log(r), e => console.log(e))
/ / () = > {}
// done
Copy the code

conclusion

In this article we introduced the basic usage and considerations of the Generator function, gave a practical example of how to use the Generator function to describe our asynchronous process, and briefly introduced the use of the Async function. All in all, ES6 also provides more ways to manage asynchronous processes, making our code organized more clearly and efficiently!