preface

If you have to write async function implementation, do you feel complicated? This article takes you to the heart of it in 20 lines.

Async is often said to be a syntactic sugar for generator functions. Let’s peel off the icing layer by layer.

Why implement async when you use generator functions?

The purpose of this article is to give you an understanding of how async and Generator work together to manage asynchrony.

The sample

const getData = (a)= > new Promise(resolve= > setTimeout((a)= > resolve("data"), 1000))

async function test() {
  const data = await getData()
  console.log('data: ', data);
  const data2 = await getData()
  console.log('data2: ', data2);
  return 'success'
}

// Such a function should print data after 1 second, data2 after 1 second, and success
test().then(res= > console.log(res))
Copy the code

Train of thought

For this simple case, what if we expressed it as a generator?

function* testG() {
  // await is compiled to yield
  const data = yield getData()
  console.log('data: ', data);
  const data2 = yield getData()
  console.log('data2: ', data2);
  return 'success'
}
Copy the code

As we know, generator functions do not execute automatically, and each call to its next method stops at the next yield.

Using this feature, we can make the generator function fully perform the async function by writing a function that executes automatically.

const getData = (a)= > new Promise(resolve= > setTimeout((a)= > resolve("data"), 1000))
  
var test = asyncToGenerator(
    function* testG() {
      // await is compiled to yield
      const data = yield getData()
      console.log('data: ', data);
      const data2 = yield getData()
      console.log('data2: ', data2);
      return 'success'
    }
)

test().then(res= > console.log(res))
Copy the code

So the general idea is set,

AsyncToGenerator takes a generator function and returns a Promise,

The key is how the asynchronous process, which is yield partitioned, should be executed automatically.

If the command is executed manually

Before writing this function, we simulate calling the generator function manually to walk through the process step by step, which is helpful for later thinking.

function* testG() {
  // await is compiled to yield
  const data = yield getData()
  console.log('data: ', data);
  const data2 = yield getData()
  console.log('data2: ', data2);
  return 'success'
}
Copy the code

We first call testG to generate an iterator

// Returns an iterator
var gen = testG()
Copy the code

Then start the first next

// The first call to next stops at the first yield
// Return a promise containing the data required by data
var dataPromise = gen.next()
Copy the code

This returns a promise, the promise returned by getData() the first time, notice

const data = yield getData()
Copy the code

This code is going to be cut right and left, so the first time I call next, I’m just going to yield getData(),

The value of data has not been determined.

So when will the value of data be determined?

The next time you call next, the parameter passed will be used as the value accepted before the last yield

That is, when we call Gen.next again (‘ this parameter will be assigned to the data variable ‘)

The value of data will be determined as’ this parameter will be assigned to the data variable ‘

gen.next('This parameter will be assigned to the data variable.')

// Then data has a value
const data = yield getData()

// Then print out data
console.log('data: ', data);

// Then continue to the next yield
const data2 = yield getData()
Copy the code

Then the process continues until the next yield is reached…

This is one of the more difficult aspects of generator design, but it will have to be learned for our purposes

With this feature, if we control the yield flow in this way, can we achieve asynchronous serialization?

function* testG() {
  // await is compiled to yield
  const data = yield getData()
  console.log('data: ', data);
  const data2 = yield getData()
  console.log('data2: ', data2);
  return 'success'
}

var gen = testG()

var dataPromise = gen.next()

dataPromise.then((value1) = > {
    // The value of data1 is retrieved and next is called and passed to data
    var data2Promise = gen.next(value1)
    
    // console.log('data: ', data);
    // Data will be printed
    
    data2Promise.value.then((value2) = > {
        // Call next and pass value2
         gen.next(value2)
         
        // console.log('data2: ', data2);
        // Data2 will be printed})})Copy the code

This call, which looks like callback Hell, allows our generator function to make the asynchronous arrangement explicit.

implementation

With this in mind, it becomes easy to implement the higher-order function.

Let’s take a look at the structure, get an impression, and then we’ll go through it line by line.

function asyncToGenerator(generatorFunc) {
    return function() {
      const gen = generatorFunc.apply(this.arguments)
      return new Promise((resolve, reject) = > {
        function step(key, arg) {
          let generatorResult
          try {
            generatorResult = gen[key](arg)
          } catch (error) {
            return reject(error)
          }
          const { value, done } = generatorResult
          if (done) {
            return resolve(value)
          } else {
            return Promise.resolve(value).then(val= > step('next', val), err => step('throw', err))
          }
        }
        step("next")}}}Copy the code

No more, no less, 22 lines.

So let’s go line by line.

function asyncToGenerator(generatorFunc) {
  // Returns a new function
  return function() {
  
    // Call generator to generate iterators
    Var gen = testG()
    const gen = generatorFunc.apply(this.arguments)

    // Return a promise because the outside is using the return value of this function either as.then or await
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) = > {
    
      // Internally define a step function to override the yield barrier step by step
      // Key has two values, next and throw, corresponding to gen's next and throw methods respectively
      The arg argument is used to yield the promise resolve value to the next yield
      function step(key, arg) {
        let generatorResult
        
        // This method needs to be wrapped in a try catch
        // If an error occurs, discard the promise, reject the error, and catch the error
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        // gen.next() results in a {value, done} structure
        const { value, done } = generatorResult

        if (done) {
          // If this is already done, resolve the promise
          // This done will not be true until the last call to next
          {done: true, value: 'success'}
          // This value is the return value of the generator function
          return resolve(value)
        } else {
          // Call gen.next() every time except at the end
          {value: Promise, done: false}
          Resolve accepts a Promise as an argument
          // Then will only be called when the promise argument is resolved
          return Promise.resolve(
            // This value corresponds to the promise after yield
            value
          ).then(
            // When the value promise is resove, next is executed
            // And whenever done is not true, the promise is recursively unwrapped
            // Next ().value.then(value => {
            // gen.next(value).value.then(value2 => {
            // gen.next()
            //
            // // now done is true and the entire promise is resolved
            // // the most external test().then(res => console.log(res)) then starts execution
            / /})
            // })
            function onResolve(val) {
              step("next", val)
            },
            // If promise is rejected, enter step again
            // The difference is that this try catch calls Gen. throw(err).
            // Then you catch the promise, reject it
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")}}}Copy the code

The source address

This js file code can be directly put into the browser to run, welcome teasing.

conclusion

This article uses the simplest way to implement the asyncToGenerator function, which is the core of Babel compiling async functions. Of course, in Babel, the generator function is also compiled into a very primitive form, and in this article we directly replace it with generator.

This is also a great model for implementing the Promise serial, so if this article helped you, just give it a thumbs up.

❤️ thank you

1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.

2. Pay attention to the front end of the public number from the advanced to the hospital! Do not regularly push high-quality original articles oh.