async、await 的前世今生

77 阅读4分钟

最近面试被问到一个很常见的八股文:说说你对promise的了解,在我一顿吹嘘下,面试官又接着问了一个问题——async、await的原理是什么? 这个问题让我很懵逼,平常只会用async、await,但是原理却没有去了解过;于是就有了这篇文章!!

回调函数

首先,要了解async、await,就要从JavaScript的异步说起了,首先引入一个问题

如果一个函数无法立即返回 value,而是经过一段不可预测的行为时间之后(副作用),才能得到 value 我们要如何做才能获得 value?

function ordinary () { 
    const i = value 
    // 
    ... return value 
} 

function sideEffect () { 
    const value = 1 
    setTimeout(() => { return value }) 
} 

console.log(ordinary()) // 1 
console.log(sideEffect()) // undefined

让我们来优化一下sideEffect函数

function sideEffect (callback) { 
    const value = 1 
    setTimeout(() => { // ... callback(value) }) 
} 

sideEffect(value => { console.log(value) // 1 })

上面就是采用了回调函数,那什么是回调函数呢?

回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

callback 让我们拥有了获取不可预测行为结果的能力,这得益于 JavaScript 函数是一等公民

Promise

但是callback 带来了很严重的语法层面上的问题——回调地狱

getData(function (a) {
  getMoreData(a, function (b) {
    getMoreData(b, function (c) {
      getMoreData(c, function (d) {
        // ...
      })
    })
  })
})

于是社区陆续出来了 promise 和类 promise 的方案(JQuery1.5 中就有了 deferred 的概念)

通过 promise 的形式重写上面的代码:

// 将 callback 变成了一种扁平化的结构
// 相对于 callback 是更加同步的思维将代码结构铺开来

getData()
  .then(getMoreData)
  .then(getMoreData)
  .then(getMoreData)
  .then(function (d) {
    // ...
  })

Promise 实际上是利用编程技巧将回调函数改成链式调用,避免回调地狱。最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

async、await

promise 链式调用的语法还是不够同步,怎么办?

const getData = () => { 
    return new Promise(resolve => resolve(1)) 
} 

const getMoreData = value => { 
    return value + 1 
} 

getData() 
    .then(getMoreData) 
    .then(getMoreData) 
    .then(getMoreData) 
    .then(value => { 
        console.log(value) // 4 
     })

分析 async、await 实现原理之前,先介绍下generator和co

generator

generator 函数是协程在 ES6 的实现。协程简单来说就是多个线程互相协作,完成异步任务。

整个 generator 函数就是一个封装的异步任务,异步操作需要暂停的地方,都用 yield 语句注明。generator 函数的执行方法如下:

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 }

co函数库

每次执行 generator 函数时自己写启动器比较麻烦。co函数库 是一个 generator 函数的自启动执行器,使用条件是 generator 函数的 yield 命令后面,只能是 thunk 函数或 Promise 对象,co 函数执行完返回一个 Promise 对象。

function co (fn, ...args) { 
    return new Promise((resolve, reject) => { 
        const gen = fn(...args) 
        
        function next (result) { ... } 
        
        function onFulfilled (res) { ... } 
        
        function onRejected (err) { ... } 
        
        onFulfilled() 
    }) 
}
// 自动调用 gen.next() 
// 然后调用 next() 将结果传入到 generator 对象内部 
function onFulfilled (res) { 
    let result try { 
        result = gen.next(res) 
        next(result) 
    } catch (err) { 
        return reject(err) 
    }
}

// 发生错误调用 gen.throw() 
// 这可以让 generator 函数内部的 try/catch 捕获到 
function onRejected (res) { 
    let result try { 
        result = gen.throw(err) next(result) 
    } catch (err) { 
        return reject(err) 
    }
}

// 接受到结果后再次调用 onFulfilled 
// 继续执行 generator 内部的代码 
function next (result) { 
    let value = result.value 
    if (result.done) return resolve(value) 
    
    // 如果是 generator 函数,等待整个 generator 函数执行完毕 
    if ( value && value.constructor && value.constructor.name === 'GeneratorFunction' ) 
    { value = co(value) } 
    
    // 转为 promise 
    Promise.resolve(value).then(onFulfilled, onRejected) 
}
  • 在这个名叫 co 的自执行函数里面
  • onFulfilled 调用 next
  • next 调用 onFulfilled
  • 这样就形成一个自执行器,只有当代码全部执行完毕后才会终止

async

async 函数是什么?它就是 Generator 函数的语法糖

做个对比,理解一下为什么说是语法糖

const ret = (async function () { 
    const a = await fn(1) 
    const b = await fn(a + 1) 
    const c = await fn(b + 1) 
    const d = await fn(c + 1) return d 
})() 
ret.then(v => console.log(v))

const ret = co(function * () { 
    const a = yield fn(1) 
    const b = yield fn(a + 1) 
    const c = yield fn(b + 1) 
    const d = yield fn(c + 1) return d 
}) 
ret.then(v => console.log(v))

总结

不论以上哪种方式,都没有改变 JavaScript 单线程、使用回调处理异步任务的本质。人类总是追求最简单易于理解的编程方式。

参考文章

文章1

文章2