最近面试被问到一个很常见的八股文:说说你对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 单线程、使用回调处理异步任务的本质。人类总是追求最简单易于理解的编程方式。