When I first got to know async/await, I was attracted by its suspended execution feature. I thought I was very curious that await can suspend the current method without native API support. Curiosity drove me to peel back everything about ASYNCHRONOUS JS programming layer by layer. By the end of this article, the reader should be able to understand:

  1. PromiseImplementation principle of
  2. async/awaitImplementation principle of
  3. GeneratorImplementation principle of

Promise to realize

In the writing process, THE author looked up a lot of articles on the implementation of Promise, but felt that most of the articles were difficult to be called clear. Some enlarged the section of Promise to standardize the translation, some wasted space on the basis of the use of Promise, or put a simple thing in a long, excessive explanation, I recommend the first iron students to pull directly to the chapter summary to see the final implementation, combined with comments directly nibble code can also understand nine out of ten

Getting back to the basics, let’s start by highlighting what Promise solves for us: In traditional asynchronous programming, if asynchronous dependencies between, we need to meet this dependence, through layers of nested callbacks if nested layers is overmuch, have become very poor readability and maintainability, produce the so-called “callback hell”, and Promise will change for chain embedded callback invocation, readability and maintainability. Let’s implement a Promise step by step:

1. Observer mode

Let’s start with the simplest Promise to use:

const p1 = new Promise((resolve, reject) = > {
    setTimeout(() = > {
        resolve('result')},1000);
}) 

p1.then(res= > console.log(res), err= > console.log(err))
Copy the code

Looking at this example, let’s examine the Promise invocation flow:

  • PromiseThe constructor of theexecutor()In thenew Promise()Execute the executor callback immediately
  • executor()Internal asynchronous tasks are put into macro/micro task queues, waiting to execute
  • then()Is executed, the success/failure callback is collected, and placed in the success/failure queue
  • executor()The asynchronous task is executed and triggeredresolve/reject, fetching the callback from the success/failure queue and executing it in sequence

In fact, students familiar with design patterns can easily realize that this is an observer pattern. This method of collecting dependencies -> triggering notification -> extracting dependency execution is widely used in the implementation of the observer pattern. In the Promise, The order of execution is then collect dependencies -> asynchronously trigger resolve -> resolve execute dependencies. From there, we can outline the shape of promises:

class MyPromise {
  // The constructor receives a callback
  constructor(executor) {
    this._resolveQueue = []    // then collection of successful callback queues
    this._rejectQueue = []     // then collection of failed callback queues

    // Since resolve/reject is called inside the executor, use the arrow function to fix this otherwise this._resolveQueue will not be found
    let _resolve = (val) = > {
      // The callbacks from the success queue are executed in sequence
      while(this._resolveQueue.length) {
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // Implement the same as resolve
    let _reject = (val) = > {
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // Execute executor immediately when new Promise() is passed in resolve and reject
    executor(_resolve, _reject)
  }

  // then method, which receives a successful callback and a failed callback and pushes it into the corresponding queue
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}
Copy the code

After writing the code, we can test it:

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('result')},1000);
})
p1.then(res= > console.log(res))
// Output result after one second
Copy the code

We implemented then and resolve in a simple way using the observer pattern, allowing us to retrieve the return value of an asynchronous operation in a callback to the THEN method. However, we are still a long way from implementing this Promise.

2. Promise A+ specification

We’ve simply implemented an ultra-low-spec Promise above, but we’ll see A lot of articles that are different from what we’ve written. They also introduce various state controls into their Promise implementations, because ES6 Promise implementations need to follow the Promise/A+ specification. It is the specification that requires state control for promises. The Promise/A+ specification is long, and there are only two core rules summarized here:

  1. Promise is essentially a state machine, and the states can only be the following three:Pending,This is a big pity.,Rejected (= Rejected), the state change is one-way, and can only be irreversible from Pending -> depressing or Pending -> Rejected
  2. Then methodReceives two optional parameters, one for the callback triggered when the state changes. The then method returns a promise. The then method can be called multiple times by the same promise.

According to the spec, we’ll add the Promise code:

//Promise/A+ three states of the specification
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // The constructor receives a callback
  constructor(executor) {
    this._status = PENDING     / / Promise
    this._resolveQueue = []    // Success queue. Resolve is triggered
    this._rejectQueue = []     // Queue failure, reject

    // Since resolve/reject is called inside the executor, use the arrow function to fix this otherwise this._resolveQueue will not be found
    let _resolve = (val) = > {
      if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
      this._status = FULFILLED              // Change the state

      // We use a queue to store callbacks in order to implement the specification that "then methods can be called more than once by the same promise"
      If a variable is used instead of a queue to store callbacks, only one callback will be executed even if p1.then() is repeated
      while(this._resolveQueue.length) {    
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // Implement the same as resolve
    let _reject = (val) = > {
      if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
      this._status = REJECTED               // Change the state
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // Execute executor immediately when new Promise() is passed in resolve and reject
    executor(_resolve, _reject)
  }

  The then method receives a successful callback and a failed callback
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}
Copy the code

3. Chain calls to THEN

After completing the specification, we will implement the chain invocation. This is the key and difficult part of the Promise implementation. Let’s first look at how the then chain invocation works:

const p1 = new Promise((resolve, reject) = > {
  resolve(1)
})

p1
  .then(res= > {
    console.log(res)
    // Then callback can return a Promise
    return new Promise((resolve, reject) = > {
      setTimeout(() = > {
        resolve(2)},1000);
    })
  })
  .then(res= > {
    console.log(res)
    // Then callbacks can also return a value
    return 3
  })
  .then(res= > {
    console.log(res)
  })
Copy the code

The output

1
2
3
Copy the code

Let’s think about how to implement this chain call:

  1. clearly.then()We need to return a Promise in order to find the THEN method, so we wrap the return value of the THEN method as a Promise.
  2. .then()The callback needs to get the previous one.then()The return value of the
  3. .then()Return Promise (1->2->3); return Promise (2->3) We wait for the current Promise state to change before executing the next THEN collection callback, which requires a categorized discussion of the return value of the THEN
/ / then method
then(resolveFn, rejectFn) {
  Return a new promise
  return new MyPromise((resolve, reject) = > {
    // Rewrap resolveFn and push it into the resolve queue to retrieve the return value of the callback for sorting
    const fulfilledFn = value= > {
      try {
        // Execute the success callback for the first (current)Promise and get the return value
        let x = resolveFn(value)
        // The class discusses the return value, if it is a Promise, then wait for the Promise state to change, otherwise resolve
        If resolve is returned by the next.then() callback, the chained call will be implemented
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
    // Push subsequent then-collected dependencies into the current Promise's success callback queue (_rejectQueue) to ensure sequential invocations
    this._resolveQueue.push(fulfilledFn)

    / / reject again
    const rejectedFn  = error= > {
      try {
        let x = rejectFn(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
    this._rejectQueue.push(rejectedFn)
  })
}
Copy the code

Then we can test the chain call:

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(1)},500);
})

p1
  .then(res= > {
    console.log(res)
    return 2
  })
  .then(res= > {
    console.log(res)
    return 3
  })
  .then(res= > {
    console.log(res)
  })

// Output 1, 2, 3
Copy the code


4. Value penetration & condition where the state has changed

We’ve done the initial chain call, but there are two more details we need to work out for the then() method

  1. Value penetration: According to the specification, if then() receives a parameter other than function, then we should ignore it. If not ignored, an exception will be thrown when the then() callback is not function, causing the chain call to break
  2. Process the resolve/ Reject statusThen (); then()paddingIn some cases, resolve/reject is executed before then() (e.gPromise.resolve().then()If the then() callback is pushed into the resolve/reject queue, then the callback will not be executed, so the state has becomefulfilledorrejectedIn this case, we execute the THEN callback directly:
The then method receives a successful callback and a failed callback
  then(resolveFn, rejectFn) {
    // According to the specification, if the argument to then is not function, we need to ignore it and let the chain call continue
    typeofresolveFn ! = ='function' ? resolveFn = value= > value : null
    typeofrejectFn ! = ='function' ? rejectFn = reason= > {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
  
    Return a new promise
    return new MyPromise((resolve, reject) = > {
      // Rewrap resolveFn and push it into the resolve queue to retrieve the return value of the callback for sorting
      const fulfilledFn = value= > {
        try {
          // Execute the success callback for the first (current)Promise and get the return value
          let x = resolveFn(value)
          // The class discusses the return value, if it is a Promise, then wait for the Promise state to change, otherwise resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      / / reject again
      const rejectedFn  = error= > {
        try {
          let x = rejectFn(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      switch (this._status) {
        // When the status is pending, push the then callback into the resolve/reject queue for execution
        case PENDING:
          this._resolveQueue.push(fulfilledFn)
          this._rejectQueue.push(rejectedFn)
          break;
        // Execute the then callback when the state has changed to resolve/reject
        case FULFILLED:
          fulfilledFn(this._value)    // this._value is the value of the previous then callback return (see the full version of the code)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break; }})}Copy the code


5. Compatible with synchronization tasks

Once we’ve done the chain call to THEN, we’ll work on one of the preceding details and then release the full code. As we said earlier, promises are executed in the order new Promise -> then() collect callbacks -> resolve/reject callbacks. This order is based on the premise that executors are asynchronous tasks. If executors are synchronous tasks, New Promise -> resolve/reject callback -> then() collect the callback. Resolve runs before THEN. We give the resolve/ Reject callback package a setTimeout to execute asynchronously.

In addition, there is a lot of knowledge about setTimeout. While the specification doesn’t say whether callbacks should be placed in a macro or microtask queue, the default implementation of Promise is actually put in a microtask queue, Our implementation (including most Promise manual implementations and polyfill transformations) uses setTimeout to queue macro tasks (of course we can also simulate microtasks using MutationObserver)

// A+ promises three states
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // The constructor receives a callback
  constructor(executor) {
    this._status = PENDING     / / Promise
    this._value = undefined    // Store the value of the then callback return
    this._resolveQueue = []    // Success queue. Resolve is triggered
    this._rejectQueue = []     // Queue failure, reject

    // Since resolve/reject is called inside the executor, use the arrow function to fix this otherwise this._resolveQueue will not be found
    let _resolve = (val) = > {
      // Encapsulate the resolve callback into a function and place it in setTimeout to accommodate cases where executors are synchronous
      const run = () = > {
        if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
        this._status = FULFILLED              // Change the state
        this._value = val                     // Store the current value

        // We use a queue to store callbacks in order to implement the specification that "then methods can be called more than once by the same promise"
        If a variable is used instead of a queue to store callbacks, only one callback will be executed even if p1.then() is repeated
        while(this._resolveQueue.length) {    
          const callback = this._resolveQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // Implement the same as resolve
    let _reject = (val) = > {
      const run = () = > {
        if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
        this._status = REJECTED               // Change the state
        this._value = val                     // Store the current value
        while(this._rejectQueue.length) {
          const callback = this._rejectQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // Execute executor immediately when new Promise() is passed in resolve and reject
    executor(_resolve, _reject)
  }

  The then method receives a successful callback and a failed callback
  then(resolveFn, rejectFn) {
    // According to the specification, if the argument to then is not function, we need to ignore it and let the chain call continue
    typeofresolveFn ! = ='function' ? resolveFn = value= > value : null
    typeofrejectFn ! = ='function' ? rejectFn = reason= > {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
  
    Return a new promise
    return new MyPromise((resolve, reject) = > {
      // Rewrap resolveFn and push it into the resolve queue to retrieve the return value of the callback for sorting
      const fulfilledFn = value= > {
        try {
          // Execute the success callback for the first (current)Promise and get the return value
          let x = resolveFn(value)
          // The class discusses the return value, if it is a Promise, then wait for the Promise state to change, otherwise resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      / / reject again
      const rejectedFn  = error= > {
        try {
          let x = rejectFn(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      switch (this._status) {
        // When the status is pending, push the then callback into the resolve/reject queue for execution
        case PENDING:
          this._resolveQueue.push(fulfilledFn)
          this._rejectQueue.push(rejectedFn)
          break;
        // Execute the then callback when the state has changed to resolve/reject
        case FULFILLED:
          fulfilledFn(this._value)    // this._value is the value of the previous then callback return (see the full version of the code)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break; }}}})Copy the code

Then we can test this Promise:

const p1 = new MyPromise((resolve, reject) = > {
  resolve(1)          // Synchronize executor tests
})

p1
  .then(res= > {
    console.log(res)
    return 2          // chain call test
  })
  .then()             // Value penetration test
  .then(res= > {
    console.log(res)
    return new MyPromise((resolve, reject) = > {
      resolve(3)      // Return the Promise test
    })
  })
  .then(res= > {
    console.log(res)
    throw new Error('reject test')   / / reject test
  })
  .then(() = > {}, err= > {
    console.log(err)
  })

/ / output
/ / 1
/ / 2
/ / 3
Error: reject test
Copy the code

By now, we’ve implemented the main functionality of Promise (‘ ∀´) and the other few methods are pretty simple, and we just cleaned them up:


Promise.prototype.catch()

The catch() method returns a Promise and handles the rejection. It behaves the same as calling promise.prototype. then(undefined, onRejected).

// The catch method executes the second callback to then
catch(rejectFn) {
  return this.then(undefined, rejectFn)
}
Copy the code


Promise.prototype.finally()

The finally() method returns a Promise. At the end of the promise, the specified callback function will be executed, whether the result is fulfilled or Rejected. After finally, you can continue then. And passes the value exactly as it should to the subsequent then

/ / finally
finally(callback) {
  return this.then(
    value= > MyPromise.resolve(callback()).then(() = > value),             // myPromise. resolve performs the callback and returns the result in then to subsequent promises
    reason= > MyPromise.resolve(callback()).then(() = > { throw reason })  / / reject again)}Copy the code

Mypromise.resolve (callback()) Finally () returns a reject Promise, which changes the state of the Promise. Mypromise.resolve is used to change the state of the Promise. If finally() does not return reject Promise or throw error, remove MyPromise. Resolve is also the same.

Resources: the Promise. Prototype. Finally () of the superficial understanding


Promise.resolve()

The promise.resolve (value) method returns a Promise object resolved with the given value. If the value is promise, return the promise; If the value is thenable (that is, with the “then” method), the returned promise “follows “the thenable object and adopts its final state; Otherwise the returned promise will be fulfilled with this value. This function flattens out the multiple layers of nesting of promise-like objects.

// Static resolve method
static resolve(value) {
  if(value instanceof MyPromise) return value // According to the specification, if the argument is a Promise instance, return the instance directly
  return new MyPromise(resolve= > resolve(value))
}
Copy the code


Promise.reject()

The promise.reject () method returns a Promise object with a reason for the rejection.

Static reject method
static reject(reason) {
  return new MyPromise((resolve, reject) = > reject(reason))
}
Copy the code


Promise.all()

The promise.all (iterable) method returns an instance of a Promise that is resolved when all promises in iterable arguments are “resolved” or when the arguments do not contain the Promise; If a promise fails (Rejected), the instance calls back (reject) because of the result of the first failed promise.

// Static all method
static all(promiseArr) {
  let index = 0
  let result = []
  return new MyPromise((resolve, reject) = > {
    promiseArr.forEach((p, i) = > {
      // promise.resolve (p) is used to handle cases where the incoming value is not a Promise
      MyPromise.resolve(p).then(
        val= > {
          index++
          result[i] = val
          // After all then execution, resolve result
          if(index === promiseArr.length) {
            resolve(result)
          }
        },
        err= > {
          // When a Promise is rejected, the state of MyPromise changes to Reject
          reject(err)
        }
      )
    })
  })
}
Copy the code


Promise.race()

The promise.race (iterable) method returns a Promise that is resolved or rejected once a Promise in the iterator is resolved or rejected.

static race(promiseArr) {
  return new MyPromise((resolve, reject) = > {
    // Execute the Promise, and if one of the Promise states changes, change the new MyPromise state
    for (let p of promiseArr) {
      MyPromise.resolve(p).then(  // promise.resolve (p) is used to handle cases where the incoming value is not a Promise
        value= > {
          resolve(value)        // Resolve = new MyPromise
        },
        err= > {
          reject(err)
        }
      )
    }
  })
}
Copy the code


The complete code

// A+ promises three states
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // The constructor receives a callback
  constructor(executor) {
    this._status = PENDING     / / Promise
    this._value = undefined    // Store the value of the then callback return
    this._resolveQueue = []    // Success queue. Resolve is triggered
    this._rejectQueue = []     // Queue failure, reject

    // Since resolve/reject is called inside the executor, use the arrow function to fix this otherwise this._resolveQueue will not be found
    let _resolve = (val) = > {
      // Encapsulate the resolve callback into a function and place it in setTimeout to accommodate cases where executors are synchronous
      const run = () = > {
        if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
        this._status = FULFILLED              // Change the state
        this._value = val                     // Store the current value

        // We use a queue to store callbacks in order to implement the specification that "then methods can be called more than once by the same promise"
        If a variable is used instead of a queue to store callbacks, only one callback will be executed even if p1.then() is repeated
        while(this._resolveQueue.length) {    
          const callback = this._resolveQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // Implement the same as resolve
    let _reject = (val) = > {
      const run = () = > {
        if(this._status ! == PENDING)return   // The state can only be changed from pending to pity or rejected.
        this._status = REJECTED               // Change the state
        this._value = val                     // Store the current value
        while(this._rejectQueue.length) {
          const callback = this._rejectQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // Execute executor immediately when new Promise() is passed in resolve and reject
    executor(_resolve, _reject)
  }

  The then method receives a successful callback and a failed callback
  then(resolveFn, rejectFn) {
    // According to the specification, if the argument to then is not function, we need to ignore it and let the chain call continue
    typeofresolveFn ! = ='function' ? resolveFn = value= > value : null
    typeofrejectFn ! = ='function' ? rejectFn = reason= > {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
  
    Return a new promise
    return new MyPromise((resolve, reject) = > {
      // Rewrap resolveFn and push it into the resolve queue to retrieve the return value of the callback for sorting
      const fulfilledFn = value= > {
        try {
          // Execute the success callback for the first (current)Promise and get the return value
          let x = resolveFn(value)
          // The class discusses the return value, if it is a Promise, then wait for the Promise state to change, otherwise resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      / / reject again
      const rejectedFn  = error= > {
        try {
          let x = rejectFn(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      switch (this._status) {
        // When the status is pending, push the then callback into the resolve/reject queue for execution
        case PENDING:
          this._resolveQueue.push(fulfilledFn)
          this._rejectQueue.push(rejectedFn)
          break;
        // Execute the then callback when the state has changed to resolve/reject
        case FULFILLED:
          fulfilledFn(this._value)    // this._value is the value of the previous then callback return (see the full version of the code)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break; }})}// The catch method executes the second callback to then
  catch(rejectFn) {
    return this.then(undefined, rejectFn)
  }

  / / finally
  finally(callback) {
    return this.then(
      value= > MyPromise.resolve(callback()).then(() = > value),             // Perform a callback and returnValue is passed to the following THEN
      reason= > MyPromise.resolve(callback()).then(() = > { throw reason })  / / reject again)}// Static resolve method
  static resolve(value) {
    if(value instanceof MyPromise) return value // According to the specification, if the argument is a Promise instance, return the instance directly
    return new MyPromise(resolve= > resolve(value))
  }

  Static reject method
  static reject(reason) {
    return new MyPromise((resolve, reject) = > reject(reason))
  }

  // Static all method
  static all(promiseArr) {
    let index = 0
    let result = []
    return new MyPromise((resolve, reject) = > {
      promiseArr.forEach((p, i) = > {
        // promise.resolve (p) is used to handle cases where the incoming value is not a Promise
        MyPromise.resolve(p).then(
          val= > {
            index++
            result[i] = val
            if(index === promiseArr.length) {
              resolve(result)
            }
          },
          err= > {
            reject(err)
          }
        )
      })
    })
  }

  // Static race method
  static race(promiseArr) {
    return new MyPromise((resolve, reject) = > {
      // Execute the Promise, and if one of the Promise states changes, change the new MyPromise state
      for (let p of promiseArr) {
        MyPromise.resolve(p).then(  // promise.resolve (p) is used to handle cases where the incoming value is not a Promise
          value= > {
            resolve(value)        // Resolve = new MyPromise
          },
          err= > {
            reject(err)
          }
        )
      }
    })
  }
}
Copy the code

After more than 150 lines of code, we’re finally ready to wrap up the Promise implementation. We start with A simple Promise use example, through the analysis of the call flow, implement the rough skeleton of the Promise according to the observer pattern, then fill in the code according to the Promise/A+ specification, focus on the implementation of the then chain call, and finally complete the static/instance method of the Promise. In fact, the Promise implementation on the whole is not too complex ideas, but we often ignore a lot of Promise details in daily use, so it is difficult to write a standardized Promise implementation, source code implementation process, in fact, is the process of relearning the use of Promise details.

Async/await

While we’ve spent a lot of time talking about Promise implementation, exploring the mechanism for suspending async/await execution is what we started off with, so let’s get into that. Again, we start with the async/await meaning. In multiple callback dependent scenarios, although Promise replaced callback nesting by chain invocation, excessive chain invocation is still not readable and the flow control is not convenient. The async function proposed in ES7 finally provides JS with the ultimate solution for asynchronous operation, which solves the above two problems in a concise and elegant way.

Imagine a scenario where there are dependencies between asynchronous tasks A -> B -> C. If we handle these relationships through then chain calls, the readability is not very good. If we want to control one of these processes, for example, under certain conditions, B does not go down to C, then it is not very easy to control either

Promise.resolve(a)
  .then(b= > {
    // do something
  })
  .then(c= > {
    // do something
  })
Copy the code

But if this scenario is implemented with async/await, readability and flow control will be much easier.

async() = > {const a = await Promise.resolve(a);
  const b = await Promise.resolve(b);
  const c = await Promise.resolve(c);
}
Copy the code


So how do we implement an async/await? First we need to know that async/await is actually a wrapper around a Generator, which is a syntactic sugar. As Generator has been replaced by async/await soon, many students are unfamiliar with Generator, so let’s take a look at its usage:

ES6 introduces Generator functions that suspend the execution flow of functions using the yield keyword and switch to the next state using the next() method, providing the possibility to change the execution flow and thus providing a solution for asynchronous programming.

function* myGenerator() {
  yield '1'
  yield '2'
  return '3'
}

const gen = myGenerator();  // Get the iterator
gen.next()  //{value: "1", done: false}
gen.next()  //{value: "2", done: false}
gen.next()  //{value: "3", done: true}
Copy the code

Yield can also be given a return value by passing an argument to next()

function* myGenerator() {
  console.log(yield '1')  //test1
  console.log(yield '2')  //test2
  console.log(yield '3')  //test3
}

// Get the iterator
const gen = myGenerator();

gen.next()
gen.next('test1')
gen.next('test2')
gen.next('test3')
Copy the code

️ should be familiar with the use of Generator. */yield and async/await look very similar in that they both provide the ability to pause execution, but there are three differences:

  • async/awaitThe built-in actuator automatically executes the next step without manually calling next()
  • asyncThe function returns a Promise object and the Generator returns a Generator object
  • awaitCan return the resolve/ Reject value of Promise

Our implementation of async/await corresponds to the above three encapsulation generators

1. Automatic execution

Let’s take a look at the process of manual execution for such a Generator

function* myGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

// Execute iterators manually
const gen = myGenerator()
gen.next().value.then(val= > {
  console.log(val)
  gen.next().value.then(val= > {
    console.log(val)
    gen.next().value.then(val= > {
      console.log(val)
    })
  })
})

// Output 1, 2, 3
Copy the code

Yield can also return resolve by passing a value to Gen.next ()

function* myGenerator() {
  console.log(yield Promise.resolve(1))   / / 1
  console.log(yield Promise.resolve(2))   / / 2
  console.log(yield Promise.resolve(3))   / / 3
}

// Execute iterators manually
const gen = myGenerator()
gen.next().value.then(val= > {
  // console.log(val)
  gen.next(val).value.then(val= > {
    // console.log(val)
    gen.next(val).value.then(val= > {
      // console.log(val)
      gen.next(val)
    })
  })
})
Copy the code

Obviously, manual execution looks awkward and ugly. We want generator functions to automatically execute down and yield to return resolve. Based on these two requirements, we do a basic wrapper where async/await is the keyword and cannot be overridden.

function run(gen) {
  var g = gen()                     // Since gen() gets the latest iterator each time, the iterator must be fetched before _next(), otherwise an infinite loop will occur

  function _next(val) {             // Encapsulate a method that recursively executes g.ext ()
    var res = g.next(val)           // Get the iterator object and return the value of resolve
    if(res.done) return res.value   // Recursive termination condition
    res.value.then(val= > {         //Promise's then method is a prerequisite for automatic iteration
      _next(val)                    // Wait for the Promise to complete and automatically execute the next, passing in the value of resolve
    })
  }
  _next()  // This is the first execution
}
Copy the code

For our previous example, we could do this:

function* myGenerator() {
  console.log(yield Promise.resolve(1))   / / 1
  console.log(yield Promise.resolve(2))   / / 2
  console.log(yield Promise.resolve(3))   / / 3
}

run(myGenerator)
Copy the code

This gives us an initial implementation of async/await. The above code is only five or six lines long, but it is not easy to understand. We have used four examples to help readers understand the code better. In simple terms, we encapsulate a run method that encapsulates the next operation as _next(), and executes _next() every time promise.then () to achieve automatic iteration. During the iteration, we also pass the value of resolve to gen.next(), allowing yield to return the Promise’s value of resolve

By the way, is it only the.then method that does what we do automatically? The thunk function is not a new thing. The thunk function is a single-parameter function that accepts only callbacks. The core of both the Promise and thunk functions is the automatic execution of the Generator by passing in callbacks. Thunk function only as an extension of knowledge, students who have difficulty in understanding can also skip this, does not affect the subsequent understanding.

2. Return Promise & exception handling

While we have implemented automatic execution of Generator and yield returning resolve, there are a few problems with the above code:

  1. Need to be compatible with base types: This code can be executed automatically only ifyieldThe Promise is followed by the Promise. In order to be compatible with cases where a value of a primitive type is followed by a value, we need to combine yield with the content (gen().next.value) withPromise.resolve()Translate it again
  2. Lack of error handlingIf the Promise in the code above fails to execute, it will interrupt the subsequent execution directlyGenerator.prototype.throw()Throw the error to be caught by the outer try-catch
  3. The return value is Promise:async/awaitThe return value is a Promise, and we need to be consistent here by giving the return value package a Promise

Let’s modify the run method:

function run(gen) {
  // Wrap the return value as a promise
  return new Promise((resolve, reject) = > {
    var g = gen()

    function _next(val) {
      // Error handling
      try {
        var res = g.next(val) 
      } catch(err) {
        return reject(err); 
      }
      if(res.done) {
        return resolve(res.value);
      }
      //res.value is wrapped as a promise to accommodate cases where yield is followed by a basic type
      Promise.resolve(res.value).then(
        val= > {
          _next(val);
        }, 
        err= > {
          // Throw an error
          g.throw(err)
        });
    }
    _next();
  });
}
Copy the code

Then we can test it:

function* myGenerator() {
  try {
    console.log(yield Promise.resolve(1)) 
    console.log(yield 2)   / / 2
    console.log(yield Promise.reject('error'))}catch (error) {
    console.log(error)
  }
}

const result = run(myGenerator)     //result is a Promise
// Output 1, 2 error
Copy the code

At this point, an async/await implementation is almost complete. Async /await = async/await = async/await = async/await = async/await

// equivalent to our run()
function _asyncToGenerator(fn) {
  // return a function, consistent with async. Our run executes Generator directly, which is not quite canonical
  return function() {
    var self = this
    var args = arguments
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args);

      // equivalent to our _next()
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      // Handle exceptions
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw); }}Copy the code

Usage:

const foo = _asyncToGenerator(function* () {
  try {
    console.log(yield Promise.resolve(1))   / / 1
    console.log(yield 2)                    / / 2
    return '3'
  } catch (error) {
    console.log(error)
  }
})

foo().then(res= > {
  console.log(res)                          / / 3
})
Copy the code

So much for the async/await implementation. However, we do not know how await is suspended until the end, and we have to look into the implementation of Generator to find out the secret of await

The Generator to realize

Starting with a simple Generator example, we will explore the implementation principle of Generator step by step:

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)
Copy the code

We can translate this code online at Babel to see how Generator is implemented in ES5:

"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          _context.next = 4;
          return 'result2';

        case 4:
          _context.next = 6;
          return 'result3';

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
Copy the code

The code doesn’t look very long at first glance, but if you look closely you’ll notice that there are two things you don’t recognize: RegeneratorRuntime. mark and RegeneratorRuntime. wrap, which are two methods in the Regenerator-Runtime module. The regenerator module comes from the Facebook Regenerator module. The complete code is in runtime.js. This Runtime has more than 700 lines… -_ – | |, so we can’t speak, don’t too important part of the us is simply too, focuses on suspended related part of the code

I think the effect of gnawing source code is not very good, suggest that readers pull to the end of the first look at the conclusion and brief version of the implementation, source code as a supplementary understanding

regeneratorRuntime.mark()

Regeneratorruntime.mark (foo) is called on the first line, so let’s look at the definition of the mark() method in runtime

// Runtime.js has a slightly different definition, with a bit more judgment. Here is the compiled code
runtime.mark = function(genFun) {
  genFun.__proto__ = GeneratorFunctionPrototype;
  genFun.prototype = Object.create(Gp);
  return genFun;
};
Copy the code

Edge GeneratorFunctionPrototype and Gp here we don’t know, they are defined in the runtime, but it doesn’t matter, as long as we know, mark () method for generator function (foo) binding a series of the prototype is ok, here is simply

regeneratorRuntime.wrap()

What does this method do? What does it want to wrap? Let’s look at the definition of the wrap method:

// Runtime.js has a slightly different definition, with a bit more judgment. Here is the compiled code
function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}
Copy the code

The wrap method creates a generator and inherits outerFn. Prototype; And then new a context object; The makeInvokeMethod method receives innerFn(for foo$), context, and this and attaches the return value to generator._invoke; Finally return generator. Wrap () essentially adds a _invoke method to the generator

OuterFn. Prototype, Context, makeInvokeMethod, makeInvokeMethod Let’s answer them one by one:

OuterFn. Prototype is actually genFun. Prototype,

We can see this by combining the above code

Context can be understood directly as a global object used to store various states and contexts:

var ContinueSentinel = {};

var context = {
  done: false.method: "next".next: 0.prev: 0.abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
  },
  complete: function(record, afterLoc) {
    if (record.type === "return") {
      this.rval = this.arg = record.arg;
      this.method = "return";
      this.next = "end";
    }

    return ContinueSentinel;
  },
  stop: function() {
    this.done = true;
    return this.rval; }};Copy the code

MakeInvokeMethod is defined as follows. It returns an invoke method that determines the current state and executes the next step, which is actually the next() we call

// Here is the compiled code
function makeInvokeMethod(innerFn, context) {
  // Set the state to start
  var state = "start";

  return function invoke(method, arg) {
    / / has been completed
    if (state === "completed") {
      return { value: undefined.done: true };
    }
    
    context.method = method;
    context.arg = arg;

    / / implementation
    while (true) {
      state = "executing";

      var record = {
        type: "normal".arg: innerFn.call(self, context)    // Go to the next step and get the status.
      };

      if (record.type === "normal") {
        // Determine whether the execution is complete
        state = context.done ? "completed" : "yield";

        // ContinueSentinel is an empty object,record.arg === {} then skip return to the next loop
        // When record-. arg is null, the answer is either no yield statement or a return has been made, i.e. when the switch returns a null value.
        if (record.arg === ContinueSentinel) {
          continue;
        }
        // Return value of next()
        return {
          value: record.arg,
          done: context.done }; }}}; }Copy the code

Why is generator._invoke actually gen. Next when the Runtime defines next(), next() actually returns the _invoke method

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {["next"."throw"."return"].forEach(function(method) {
      prototype[method] = function(arg) {
        return this._invoke(method, arg);
      };
    });
}

defineIteratorMethods(Gp);
Copy the code

Low profile implementation & call flow analysis

After all, there are many concepts and packages in the source code. It will not be easy to fully understand. Let’s jump out of the source code and implement a simple Generator, and then look back at the source code

// The generator function splits the code into switch-case blocks based on the yield statement, and then executes each case separately by toggling _context.prev and _context.next
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return_context.stop(); }}}// Lower version context
var context = {
  next:0.prev: 0.done: false.stop: function stop () {
    this.done = true}}// Invoke with low configuration
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
} 

// Test use
var g = gen() 
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true}
Copy the code

This code is not difficult to understand, let’s examine the call flow:

  1. We definedfunction* The generator function is converted to the above code
  2. The transformed code is divided into three chunks:
    • gen$(_context)The yield splits the generator function code
    • The context objectUsed to store the execution context of a function
    • Invoke () methodDefine next() to execute gen$(_context) to skip to the next step
  3. When we callg.next(), is equivalent to callingInvoke () method, the implementation ofgen$(_context), enter the switch statement, switch executes the corresponding case block according to the context identifier, return corresponding result
  4. When the generator function runs to the end (there is no next yield or has already returned) and the switch does not match the corresponding code block, the return is nullg.next()return{value: undefined, done: true}

The function is not actually suspended. Each yield actually executes the incoming Generator function, but a context object is used to store the context in the middle of the process, so that each time a Generator function is executed, Can be executed from the last execution result, as if the function was suspended

Summary & Acknowledgements

This is where the principles of Promise, async/await and Generator are realized. Thank you for coming with me. Before I knew it, we spent nearly 9,000 words telling stories about asynchronous programming. Later, from a small problem of “how to suspend the execution of await”, a series of thoughts and implementation principles of asynchronous programming were introduced. The realization of the three, in fact, is the process of front-end asynchronous programming step by step evolution.

These four references were selected after I read a lot of relevant articles. I suggest you combine them with reading. The leaders wrote much better than me

Front-end technician: all kinds of source code implementation, you want to have here

How did I implement the Promise Winty: async/await principle and execute sequence analysis of Hu Hu: What did the ES6 series Babel compile the Generator into


Blue Jack



The articles

  1. 10 lines of code to see all redux implementation – comprehensive analysis redux & react – redux & redux middleware design implementation | 8 k
  2. Red and black tree red and black fruit, red and black tree introduction to you and me – red and black tree | 6 k
  3. SSR in-depth the React from entry to give up, the service side rendering principle | 1 w words?