In May 2018, the 4.x version of Node has been stopped for maintenance, and a service of our company has been switched to 8.x. Currently, we are migrating koA2.x and replacing all the previous generator with Async. However, in the process of replacement, After finding some time waste caused by async abuse, let’s talk about how to optimize async code and make full use of asynchronous event stream to prevent async abuse

First, you need to know Promise

Promise is the basis for using async/await, so make sure you understand what promises do first. Promises are a great way to help with callback hell and make asynchronous processes clearer. A simple error-first-callback conversion to Promise example:

const fs = require('fs')

function readFile (fileName) {
  return new Promise((resolve, reject) = > {
    fs.readFile(fileName, (err, data) => {
      if (err) reject(err)

      resolve(data)
    })
  })
}

readFile('test.log').then(data= > {
  console.log('get data')
}, err => {
  console.error(err)
})
Copy the code

We call a function to return an instance of Promise, and read the file during the instantiation process. When the file read callback is triggered, we change the Promise state, resolved state or Rejected state. We use then to listen and the first callback is resolve. The second callback is a reject processing.

Async relationship with Promise

Async functions are equivalent to a shorthand function that returns a Promise instance, with the following effect:

function getNumber () {
  return new Promise((resolve, reject) = > {
    resolve(1)})}/ / = >
async function getNumber () {
  return 1
}
Copy the code

Both can be used in exactly the same way, using then to listen for the return value after calling getNumber. And the use of await syntax corresponding to async:

getNumber().then(data= > {
  // got data
})
/ / = >
let data = await getNumber()
Copy the code

The execution of await gets the result of the Promise execution following the expression, just as we get the callback result by calling THEN. P.S. when async/await support is not very high, people will choose to use generator/yield combined with some libraries similar to CO to achieve similar effects

Async code is executed synchronously and results are returned asynchronously

It is important that async always returns an instance of a Promise, so when an async function is called, it means that the code inside the async function is in a new Promise, so it is executed synchronously, and the last return operation is equivalent to calling resolve in the Promise:

async function getNumber () {
  console.log('call getNumber()')

  return 1
}

getNumber().then(_= > console.log('resolved'))
console.log('done')

// Output sequence:
// call getNumber()
// done
// resolved
Copy the code

Promises within promises are digested

That is, if we have code like this:

function getNumber () {
  return new Promise(resolve= > {
    resolve(Promise.resolve(1))
  })
}

getNumber().then(data= > console.log(data)) / / 1
Copy the code

The data we get in THEN should be the value passed in to Resolve, which is another instance of a Promise. But in practice, we get the return value: 1 directly, which means that if we return a Promise in a Promise, the program actually implements the Promise for us and triggers a callback like then when the internal Promise state changes. An interesting thing:

function getNumber () {
  return new Promise(resolve= > {
    resolve(Promise.reject(new Error('Test')))
  })
}

getNumber().catch(err= > console.error(err)) // Error: Test
Copy the code

If we pass a reject in resolve, we can externally listen directly with a catch. This is often used to throw an exception in an async function:

async function getNumber () {
  return Promise.reject(new Error('Test'))}try {
  let number = await getNumber()
} catch (e) {
  console.error(e)
}
Copy the code

Don’t forget the await keyword

There is no error at the code level if we forget to add the await keyword, but the return value we receive is a Promise

let number = getNumber()
console.log(number) // Promise
Copy the code

So remember the await keyword when you use it

let number = await getNumber()
console.log(number) / / 1
Copy the code

Not all places need to add await

At some point during code execution, not all asynchronies need to add await. For example, we assume that all apis of FS have been converted to the Promise version

async function writeFile () {
  let fd = await fs.open('test.log')
  fs.write(fd, 'hello')
  fs.write(fd, 'world')
  return fs.close(fd)
}
Copy the code

As mentioned above, the Promise inside the Promise will be digested, so we don’t use await in the last close. We open a file with await, and then write the file twice. Note, however, that we did not add the await keyword before the two writes to the file. Because that’s redundant, we just need to tell the API that I’m going to write a line of text into this file, and the order will be controlled by FS. Close is done last, because the callback to close will not fire if the write has not been completed. This means that the write has been completed.

Merges multiple unrelated async function calls

Now if we want to get a user’s profile picture and the user’s details (and these are two interfaces that don’t usually happen)

async function getUser () {
  let avatar = await getAvatar()
  let userInfo = await getUserInfo()

  return {
    avatar,
    userInfo
  }
}
Copy the code

One problem with this code is that our interface for retrieving user information does not depend on the return value of the avatar interface. However, such code will not send a request for user information until the avatar is obtained. So we can do this with code like this:

async function getUser () {
  let [avatar, userInfo] = await Promise.all([getAvatar(), getUserInfo()])

  return {
    avatar,
    userInfo
  }
}
Copy the code

This change would make getAvatar execute simultaneously with the code inside getUserInfo, sending two requests at the same time, and making sure both return results through a layer of packets promise.all in the outer layer.

Let asynchronous functions that do not depend on each other execute simultaneously

Some considerations in the loop

forEach

When we call code like this:

async function getUsersInfo () {[1.2.3].forEach(async uid => {
    console.log(await getUserInfo(uid))
  })
}

function getuserInfo (uid) {
  return new Promise(resolve= > {
    setTimeout(_= > resolve(uid), 1000)})}await getUsersInfo()
Copy the code

There seems to be no problem with this execution, we will get 1, 2, 3 log output, but if we add console.log(‘done’) under await getUsersInfo(), we will find: We get done first and then three uid logs, which means that when getUsersInfo returns the result, the internal Promise isn’t finished. This is because forEach doesn’t care what the return value of the callback is, it just runs the callback.

Do not use await in normal for, while loops

Using normal for, while loops causes the program to become serial:

for (let uid of [1.2.3]) {
  let result = await getUserInfo(uid)
}
Copy the code

This code will not request uid: 2 until it receives the data of UID: 1


Solutions to these two problems:

At present, it is optimal to replace it with map and Promise. All:

await Promise.all([1.2.3].map(async uid => await getUserInfo(uid)))
Copy the code

Such a code implementation instantiates three Promises simultaneously and requests getUserInfo

There is one in the P.S. draftawait*, can be omittedPromise.all

await* [1.2.3].map(async uid => await getUserInfo(uid))
Copy the code

Why is P.S. in useGenerator+coThere is no such problem

When using koa1.x, we simply yield []. Map without the serializing problems mentioned above:

function toPromise(obj) {
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  return obj;
}

function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
Copy the code

Co is the one that helped us add the promise.all processing (bow to TJ).

conclusion

To summarize a few tips on async function writing:

  1. usereturn Promise.reject()inasyncThe function throws an exception
  2. Let asynchronous functions that have no dependencies on each other execute simultaneously
  3. Do not use/in loop callbacksfor,whileUse in circulationawaitwithmapTo replace it

The resources

  1. async-function-tips