理解 Async / await

591 阅读2分钟

最近突然发现一个问题,起因是在面试的时候,有一道基础知识题,答对的寥寥无几,题目如下:

async function async1() {
  console.log('async1')
  await async2()
  console.log('async2')
}

async function async2() {
  console.log('async2')
}

async1()

new Promise((resolve, reject) => {
  console.log('promise1')
  resolve()
}).then(res => {
  console.log('promise2')
})

这题原本不难,但是一旦 async/await 与 promise 混合在一起,错误率飙升,这主要还是没有很好的理解 async / await 机制。

理解这题之前,首先要熟悉事件循环(Event Loop),对于事件循环中的宏任务与微任务,推荐看这个网址:jakearchibald.com/2015/tasks-…,内部有动画一步步执行入栈和出栈的过程。

对于 async / await 的理解,首先得理解函数前面加上 async 标记,函数会有什么变化:

async function asynctest() {
    let result = false
    console.log(result, 'result')
    return result
}

image.png

function test() {
    let result = false
    console.log(result, 'result')
    return result
}

image.png

根据上图所得,函数前面加上 async 标记,那么返回值会被包裹一层 Promise 封装。

如果不用 async 标记,如果实现同样的效果呢:

function test() {
    let result = false
    console.log(result, 'result')
    return Promise.resolve(result)
}

image.png

// PS
Promise.resolve(false)  === new Promise(res => res(false))

那么我们再加上 await 会不会有什么变化:

async function asynctest() {
    let result = await false
    console.log(result, 'result')
    return result
}

image.png

这看起来与没有加 await 没有区别,那把 await 后面改成异步函数再看看:

function syncFn() {
    return new Promise((res) => {
        setTimeout(() => {
            res('syncFn')
        }, 1000)
    })
}

async function asynctest() {
    let result = await syncFn()
    console.log(result, 'await result')
    return result
}

async.gif

以上结果可知:函数前面加了 async 标记,那么函数的返回结果会被包裹一层 Promise,而 await 可以加在一个表达式的前面,阻塞当前函数执行栈,直到表达式执行完成。await 等待只是拿到表达式返回的值。因此,如果把 async 函数改写成 Promise 的风格的写法,可以写成这样。

async function asynctest() {
    let result = await syncFn()
    console.log(result, 'await result')
    return result
}

// 改写成
function asynctest() {
    let result = new Promise(res => {
        let ret = syncFn()
        res(ret)
    }).then(res => {
        console.log(result, 'await result')
        return res
    })
    return result
}

GIF.gif

这样只用 Promise 改写,在某些场景并不好理解,比如:

function syncFn() {
    return new Promise((res) => {
        setTimeout(() => {
            res('syncFn')
        }, 1000)
    })
}
async function test(){
    for (let i = 0; i < 10; i++){
        let res = await syncFn()
        console.log(res, i)
    }
}

GIFfor2.gif

await 是可以阻塞 for 循环的,如果用 promise 改写,这又得写成地狱回调方法:

function syncFn() {
    return new Promise((res) => {
        setTimeout(() => {
            res('syncFn')
        }, 1000)
    })
}
function asynctest(i) {
    if (i > 9) return 
    let result = new Promise(res => {
        let ret = syncFn()
        res(ret)
    }).then(res => {
        console.log(res, i)
        asynctest(i + 1)
        return res
    })
}

GIF5.gif

对于阻塞函数的场景,应该很容易想到 generate 函数:

function *generate() {
    for (let i = 0; i < 5; i++) {
        let res = yield i
        console.log(res)
    }
    console.log('end')
}

GIFgenerate.gif

所以可以结合 generate 函数与 promise:

写一个自执行的 generator 函数:

// generator 自执行器
function asyncToGenerator(genFn) {
  return new Promise((resolve, reject) => {
    let gen = genFn()
    function step(key, arg) {
      let info = {}
      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error)
        return
      }
      if (info.done) {
        resolve(info.value)
      } else {
        return Promise.resolve(info.value).then(v => {
          return step('next', v)
        }, (error) => {
          return step('throw', error)
        })
      }
    }
    step('next')
  })
}

然后测试一下:

image.png

let test = function () {
  let ret = asyncToGenerator(function* () {
    for (let i = 0; i < 10; i++) {
      let result = yield syncFn();
      console.log(result, i);
    }
  })
  return ret
}

GIFforgene.gif