Please indicate the source of reprint:

Generator function asynchronous application – gold mining

Generator function asynchronous applications – Blog garden

Generator function asynchronous application – Zhihu

The last article covered the syntax of Generator functions in detail. This article will show you how to use Generator functions for asynchronous programming.

It may be rare to use Generator functions to implement asynchracy, as ECMAScript 2016 async encapsulates the flow control of Generator functions, making asynchronous schemes easier to use.

However, I personally think that before learning async functions, it is necessary to know how Generator implements async, which may be helpful for learning async functions.

The article directories

  1. A brief review of knowledge points
  2. Encapsulation of asynchronous tasks
  3. The thunk function implements flow control
  4. Automatic flow control of Generator functions
  5. Automatic process control of CO module

A brief review of knowledge points

As discussed in the Generator function syntax parsing article, Generator functions can define multiple internal states and are also iterator object generators. Yield expressions can define multiple internal states and also have the ability to pause the execution of a function. When a Generator function is called, it is not executed immediately, but returns a traverser object.

The iterator object has a next method on its prototype object that can be used to resume the execution of the function. Each time the next method is called, it stops when it encounters a yield expression, and the next time it is called, execution continues where it left off. Calling the next method returns an object with value and done attributes. The value attribute represents the current internal state. Possible values include yield expressions, return statements, and undefined. The done attribute indicates whether the traversal is complete.

By default, the yield expression returns no value, or undefined. Therefore, to get the return value of the yield expression, you need to pass arguments to the next method. The argument to the next method represents the return value of the previous yield expression. So the first next method can be called without passing an argument (it doesn’t work if it does), which means the traverser object is started. So the next method is used one more time than the yield expression.

See this article for more detailed syntax. Portal: Generator function syntax parsing

Encapsulation of asynchronous tasks

The yield expression suspends function execution, and the next method resumes it. This makes Generator functions ideal for synchronizing asynchronous tasks. Next, setTimeout is used to simulate asynchronous tasks.

const person = sex => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        sex,
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)
}
const g = gen()
const next1 = g.next() // {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})
Copy the code

As you can see from the above code, when the next method is called for the first time, the traverser object is started. At this time, the object containing value and done attribute is returned. Since value is a Promise object, the value of resolve can be obtained by using then method. The next method with a data parameter is then used to pass the return value to the previous yield expression.

Const data = yield Person (); const data = yield Person ();

But there are problems with the code above. The following steps are performed manually each time an asynchronous value is retrieved

const g = gen()
const next1 = g.next() {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})
Copy the code

The above code essentially reuses the value attribute value and the next method each time, so each time you use Generator to implement asynchracy, there are flow control issues involved. It would be a hassle to manually implement flow control every time. Is there any way to implement automatic flow control? Actually there is:)

The thunk function implements flow control

The thunk function is actually a bit like a JavaScript function corrification, passing one function as an argument to another, and then evaluating the argument (function) through a closure.

The implementation of the function is as follows

function curry (fn) {
  const args1 = Array.prototype.slice.call(arguments, 1)
  return function () {
    const args2 = Array.from(arguments)
    const arr = args1.concat(args2)
    return fn.apply(this, arr)
  }
}
Copy the code

Use the Curry function as an example:)

Const sum = (a, b) => {const sum = (a, b) => {return a + b
}
curry(sum, 1)(2)   // 3
Copy the code

The thunk function is simply implemented as follows:

// ES5 implements const thunk = fn => {return function () {
    const args = Array.from(arguments)
    return function (callback) {
      args.push(callback)
      returnFn. apply(this, args)}}} // ES6 const thunk = fn => {return function(... args) {return function (callback) {
      returnfn.call(this, ... args, callback) } } }Copy the code

From the thunk function above, you can see that the Thunk function uses one more layer of closure to encapsulate the function scope than the curried function.

Using the thunk function above, you can generate the thunk function for fs.readfile.

const fs = require('fs')
const readFileThunk = thunk(fs.readFile)
readFileThunk(fileA)(callback)
Copy the code

Thunk wraps fs.readFile as readFileThunk, and passes the file path through fileA. Callback is fs.readFile’s callback.

Of course, there is an upgraded version of the thunk function, thunkify, that makes the callback run only once. Much like the thunk function above, but with a flag parameter that limits the number of times the callback can be executed. Next I made some changes to the Thunkify function. Source code address: Node-thunkify

const thunkify = fn => {
  return function () {
    const args = Array.from(arguments)
    return function (callback) {
      let called = false// called variable limits the number of times callback can be executed args.push(function () {
        if (called) return
        called = true
        callback.apply(this, arguments)
      })
      try {
        fn.apply(this, args)
      } catch (err) {
        callback(err)
      }
    }
  }
}
Copy the code

Here’s an example:

functionsum (a, b, Callback) {const total = a + b console.log(total) console.log(total)} // If thunkify is used const sumThunkify = Thunkify(sum) sumThunkify(1, 2)(console.log) const sumThunk = thunk(sum) sumThunk(1, 2)(console.log) // Print 3, 3Copy the code

Consider another example of using setTimeout to simulate asynchrony and using the Thunkify module to accomplish asynchronous task synchronization.

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
const g = gen()
const next = g.next()
next.value(data => {
  g.next(data)
})
Copy the code

As you can see from the code above, the value property is actually a callback to thunkify (which is also the second argument to Person), while ‘boy’ is the first argument to person.

Automatic flow control of Generator functions

In the above code, we can encapsulate the process of calling the iterator object generation function, returning the iterator, and manually executing the next method to resume the function execution.

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value(next)
  }
  next()
}
Copy the code

Wrapped in run, the next function inside run is essentially a callback to the thunkify function. Therefore, the automatic flow control of the Generator can be achieved by calling run.

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
run(gen)
// {sex: 'boy', name: 'keith', height: 180}
Copy the code

With this actuator, it is much easier to execute Generator functions. Pass the Generator function to the run function, no matter how many asynchronous operations are inside. That is, of course, if every asynchronous operation is a thunkify function. That is, the yield expression must be followed by a thunk(Thunkify) function.

const gen = function *gen () {
  const f1 = yield personThunk('boy'Const f2 = yield personThunk(thunkify) const f2 = yield personThunk('boy')
  // ...
  const fn = yield personThunk('boy'} run(gen) // Automatic flow control of the run functionCopy the code

In the code above, the function gen encapsulates n asynchronous behaviors that are automatically completed by executing the run function. In this way, asynchronous operations can not only be written to look like synchronous operations, but can also be performed in a single line of code.

Automatic process control of CO module

As mentioned in the above example, the value following the expression must be the thunk(Thunkify) function to implement automatic flow control for the Generator function. The implementation of the Thunk function is based on callback functions, while the CO module goes a step further and is compatible with thunk functions and Promise objects. Let’s start with the basic usage of the CO module

const co = require('co')
const gen = function *gen () {
  const f1 = yield person('boy'Const f2 = yield person() const f2 = yield person('boy'} co(gen) // Encapsulate the thunkify (thunkify) and run functions into CO modules. Yield expressions can be followed by thunkify (thunkify) or Promise objectsCopy the code

The CO module does not need to write an executor for the Generator function because it is already wrapped. Add the Generator function to the CO module and the function will be executed automatically.

The CO function returns a Promise object, so callbacks can be added using the then method.

co(gen).then(function (){
  console.log('Generator function completed execution ')})Copy the code

Co module principle; The CO module is essentially a module that wraps two autoactuators (thunkify and Promise objects) into one. The premise for using the CO module is that only thunk(Thunkify) or Promise objects can be used after the yield expression of a Generator function. You can also use the CO module if all members of an array or object are Promise objects.

Automatic execution based on Promise objects

Use the same example, but this time change the callback function to a Promise object for automatic flow control.

const person = (sex, fn) => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)   // {name: 'keith', height: 180}
}
const g = gen()
g.next().value.then(data => {
  g.next(data)
})
Copy the code

Manual execution is essentially layering the THEN and next methods. From this you can write the autoactuators.

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value.then(data => {
      next(data)
    })
  }
  next()
}
run(gen)  // {name: 'keith', height: 180}
Copy the code

If interested in co module friends, you can read its source code. Portal: CO

That’s all I need to know about Generator asynchronous applications.

  1. Because the yield expression can suspend execution and the next method can resume execution, Generator functions are suitable for synchronizing asynchronous tasks.
  2. However, flow control for Generator functions is a bit more cumbersome, as each time you need to manually execute the next method to resume function execution and pass parameters to the next method to output the return value of the previous yiled expression.
  3. Thunk (Thunkify) function and CO module are used to realize automatic flow control of Generator function.
  4. The parameters are separated by thunk(Thunkify) function, passed in one by one in the form of closure, and then called by apply or call method, combined with run function, automatic flow control can be achieved.
  5. Through co module, run function and Thunkify (Thunkify) function are encapsulated, and yield expression supports both thunkify (Thunkify) function and Promise object, making automatic flow control more convenient.

The resources

  1. Asynchronous application of Generator functions
  2. node-thunkify
  3. co