Review of asynchronous programming in JavaScript

Because JavaScript is a single-threaded execution model, it must support asynchronous programming to be efficient. The syntactic goal of asynchronous programming is to make asynchronous procedures write like synchronous procedures.

1. Callback function

The callback function, which writes the second section of the task in a separate function, will be called when the task is executed again.

const fs = require('fs')
fs.readFile('/etc/passwd', (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data.toString())
})
Copy the code

The biggest problem with callback functions is the tendency to create callback hell, where multiple callback functions are nested, which reduces code readability, increases logic complexity, and is error-prone.

fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... })})Copy the code

2. Promise

To address the shortcomings of callback functions, the community created promises.

const fs = require('fs')

const readFileWithPromise = file => {
  return new Promise((resolve, reject) => {
    fs.readFile(file, (err, data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data)
      }
    })
  })
}

readFileWithPromise('/etc/passwd')
  .then(data => {
    console.log(data.toString())
    return readFileWithPromise('/etc/profile')
  })
  .then(data => {
    console.log(data.toString())
  })
  .catch(err => {
    console.log(err)
  })
Copy the code

Simple Promise fulfillment, peeking into the nitty-gritty

Promise actually used programming tricks to change the horizontal loading of the callback function to vertical loading, achieving the effect of a chain call and avoiding callback hell. The biggest problem is the code redundancy, the original task was packaged by Promise, no matter what operation, it is a bunch of THEN at a glance, the original semantics become very unclear.

3. The async and await

To solve the Promise problem async and await have been mentioned in ES7 and are by far the best solution

const fs = require('fs') async function readFile() { try { var f1 = await readFileWithPromise('/etc/passwd') console.log(f1.toString()) var f2 = await readFileWithPromise('/etc/profile') console.log(f2.toString()) } catch (err) {  console.log(err) } }Copy the code

Async and await functions are written like synchronous functions, provided that they receive a Promise or primitive type value. The ultimate goal of asynchronous programming is to translate into a form that is most easily understood by humans.

Async and await

Before analyzing the implementation principle of async and await, we will introduce the preparatory knowledge first

1. generator

Generator functions are an ES6 implementation of coroutines. Coroutines are simply threads working together to complete asynchronous tasks.

The entire generator function is an encapsulated asynchronous task, and where an asynchronous operation needs to be paused, use yield statements. The generator function is executed as follows:

function* gen(x) {
  console.log('start')
  const y = yield x * 2
  return y
}

const g = gen(1)
g.next()   // start { value: 2, done: false }
g.next(4)  // { value: 4, done: true }
Copy the code
  • Instead of executing immediately, gen() pauses and returns an Iterator (see the Iterator Iterator).

  • Each time g.ext () breaks the pause state and executes until the next yield or return is reached

  • When yield is encountered, the expression after yield is executed and the value after execution is returned, and the state is paused again, done: false.

  • The next function can take parameters, which are received by variables in the function body, as the return result of the asynchronous task in the previous phase

  • When a return is encountered, the value is returned and the execution ends, that is, done: true

  • Every time g.ext () returns {value:… , done: … } in the form of

2. Thunk function

The thunk function in JavaScript simply converts a multi-argument function with a callback to a single-argument version that accepts only the callback

const fs = require('fs') const thunkify = fn => (... rest) => callback => fn(... rest, callback) const thunk = thunkify(fs.readFile) const readFileThunk = thunk('/etc/passwd', 'utf8') readFileThunk((err, data) => { // ... })Copy the code

The thunk function alone is not very useful, so we decided to combine it with a generator:

function* readFileThunkWithGen() {
  try {
    const content1 = yield readFileThunk('/etc/passwd', 'utf8')
    console.log(content1)
    const content2 = yield readFileThunk('/etc/profile', 'utf8')
    console.log(content2)
    return 'done'
  } catch (err) {
    console.error(err)
    return 'fail'
  }  
}

const g = readFileThunkWithGen()
g.next().value((err, data) => {
  if (err) {
    return g.throw(err).value
  }
  g.next(data.toString()).value((err, data) => {
    if (err) {
      return g.throw(err).value
    }
    g.next(data.toString())
  })
})
Copy the code

What the thunk function really does is unify the way multi-argument functions are called, giving control back to the Generator when next is called so that the Generator function can recursively start the process itself

const run = generator => { const g = generator() const next = (err, ... rest) => { if (err) { return g.throw(err).value } const result = g.next(rest.length > 1 ? rest : rest[0]) if (result.done) { return result.value } result.value(next) } next() } run(readFileThunkWithGen)Copy the code

With self-start support, “synchronous” code can be written within generator functions. Generator functions can also be combined with promises:

function* readFileWithGen() {
  try {    
    const content1 = yield readFileWithPromise('/etc/passwd', 'utf8')
    console.log(content1)
    const content2 = yield readFileWithPromise('/etc/profile', 'utf8')
    console.log(content2)
    return 'done'
  } catch (err) {
    console.error(err)
    return 'fail'
  }
}

const run = generator => {
  return new Promise((resolve, reject) => {
    const g = generator()
    const next = res => {
      const result = g.next(res)
      if (result.done) {
        return resolve(result.value)
      }
      result.value
        .then(
          next,
          err => reject(gen.throw(err).value)
        )
    }
    next()
  })
}

run(readFileWithGen)
  .then(res => console.log(res))
  .catch(err => console.log(err))
Copy the code

The Generator can suspend execution and is easy to associate with asynchronous operations because we can suspend the current task while we wait, giving control back to another program, and give control back to the previous task in a callback when the asynchronous task returns. The Generator does not actually change the single-threaded nature of JavaScript that uses callbacks to handle asynchronous tasks.

3. Co function library

It is troublesome to write the initiator each time generator is executed. The co library is a self-starting executor of a generator function, which can only be a Thunk or Promise object after the yield command of the generator function. The CO function returns a Promise object after execution.

const co = require('co')
co(readFileWithGen).then(res => console.log(res)) // 'done'
co(readFileThunkWithGen).then(res => console.log(res)) // 'done'
Copy the code

The source code of the CO function library is actually a synthesis of the above two situations:

Const co = (generator,... rest) => { const ctx = this return new Promise((resolve, reject) => { const gen = generator.call(ctx, ... rest) if (! gen || typeof gen.next ! == 'function') { return resolve(gen) } const onFulfilled = res => { let ret try { ret = gen.next(res) } catch (e) { return reject(e) } next(ret) } const onRejected = err => { let ret try { ret = gen.throw(err) } catch (e) { return reject(e) } next(ret) } const next = result => { if (result.done) { return resolve(result.value) } toPromise(result.value).then(onFulfilled, onRejected) } onFulfilled() }) } const toPromise = value => { if (isPromise(value)) return value if ('function' == typeof value) { return new Promise((resolve, reject) => { value((err, ... rest) => { if (err) { return reject(err) } resolve(rest.length > 1 ? rest : rest[0]) }) }) } }Copy the code

Understand async, await

In short, async and await are the official implementations of the CO library. You can also think of it as a syntactic sugar for a generator function that comes with its own launcher. The difference is that async and await only support promises and primitive values, not thunk functions.

// generator with co
co(function* () {
  try {    
    const content1 = yield readFileWithPromise('/etc/passwd', 'utf8')
    console.log(content1)
    const content2 = yield readFileWithPromise('/etc/profile', 'utf8')
    console.log(content2)
    return 'done'
  } catch (err) {
    console.error(err)
    return 'fail'
  }
})

// async await
async function readfile() {
  try {
    const content1 = await readFileWithPromise('/etc/passwd', 'utf8')
    console.log(content1)
    const content2 = await readFileWithPromise('/etc/profile', 'utf8')
    console.log(content2)
    return 'done'
  } catch (err) {
    throw(err)
  }
}
readfile().then(
  res => console.log(res),
  err => console.error(err)
)
Copy the code

conclusion

Either way, it doesn’t change the single-threaded nature of JavaScript that uses callbacks to handle asynchronous tasks. Humans are always looking for the simplest and easiest way to program.

The resources

Deep understanding of JavaScript asynchronous deep understanding of ECMAScript 6 asynchronous programming