从开始的回调函数,到es6中的promise,再到async await最受喜爱,中间到底经历了什么?
回调函数
最最原始的异步操作是通过纯回调函数实现的。
getFile(syncFunc,successCallback,errorCallback);
多个串行且相互以来的异步操作如下:
// 回调地狱
getFile(data,successCallback(result1){
getFileTwo(result1,successCallbackTwo(result2){
getFileThree(result2,successCallbackThree(result3){
// 处理数据
},errorCallback3)
},errorCallback2)
},errorCallback1)
可以看出,回调方式的异步串行会造成回调地狱,很难维护,并且错误处理与正常业务代码耦合在一起,牵一发而动全身。
为了解决回调地狱,出现了Promise。
Promise与回调函数比有什么优点?(为什么要用Promise?)
针对以上情况,Promise的代码实现如下(经过简化):
function getFile(){
return new Promise((resolve,reject)=>{
const content = fs.readFileSync('')
return resolve(content)
})
}
getFile()
.then(result1=> return getFileTwo(result1))
.then(result2=> return getFileTwo(result2))
.then(result3=> 处理数据)
// 统一错误处理(错误透传)
.catch(err=>console.log(err))
可以看出,纯回调函数和promise的区别如下:
- 回调函数在写异步操作时,就必须同时写回调。而Promise可以在异步进行中/结束后的任何时候为它添加回调函数,并且获得的结果是固定的。
- Promise可以多次获取异步操作的结果,回调函数想要获取多次结果只能执行多次异步操作,并为异步操作绑定回调获取结果。
- 用promise解决回调地狱,不用多次处理异常,提高代码可读性。
Promise实现本身其实还是依赖回调,它只是通过发布订阅的方式,在异步执行的时候把回调函数进行存储,异步执行结束后再调用回调,从而实现了链式调用。
通过resolve和reject,拿到了异步函数的控制权。
生成器(Generator)
promise虽然解决了回调地狱,但是它一连串的then,在视觉上可读性还是不够好。
我想难道不能像写串行代码一样写多个异步函数吗?
像下面这样,做不到吗?
const data = getDataSync();
const data1 = showDataSync(data);
const data2 = alertDataSync(data1);
显然通过以上技术都做不到,除非让异步操作变成同步操作。
我在执行getDataSync异步函数的时候,异步结果还没得到,此时返回的data为undefined。作为相互依赖的异步操作显然是不行的。
我又想:我需要让前一个异步执行完之后,再向下执行另一个,能不能做到?
生成器函数的出现为我们提供了异步串行的一种新思想。
/*
生成器
yield:函数代码分割符,当一个异步函数结束后,调用next继续调用后面的异步
*/
function getData(){
return new Promise((resolve,reject)=>{
setTimeout(() => {
let data = '111';
// 得到第一个结果了,调用next继续
iterator.next(data)
}, 1000);
})
}
function showData(data){
return new Promise((resolve,reject)=>{
setTimeout(() => {
let a = '111';
// 得到第二个结果了,调用next继续
iterator.next(data + a)
}, 1000);
})
}
function alertData(data){
return new Promise((resolve,reject)=>{
setTimeout(() => {
let a = '111';
// 得到第三个结果了,调用next继续
iterator.next(data + a)
}, 1000);
})
}
/*
yield可以获取异步函数的执行权 通过next继续执行下一个异步操作
*/
function * gen(){
const data = yield getData();
const data1 = yield showData(data);
const data2 = yield alertData(data1);
console.log(data2)
}
const iterator = gen();
iterator.next()
上面代码块中gen函数就是生成器函数,前面的*是生成器的标识符,执行之后返回一个迭代器对象。它的特点是拥有异步函数的执行权,可以在执行一个异步函数的时候让程序无阻塞的暂停,得到结果后又可以通过迭代器对象中的next方法继续执行下一个异步,直到执行完迭代器中所有的函数。
注:为什么要让程序暂停呢?
这里的暂停主要是给异步函数执行的时间,不然我们通过const data = syncFunc()这种方式无法在异步执行时得到data
所以,有了生成器函数之后,我们的几个互相依赖的异步函数的写法,就像串行语句一样优雅,可读性非常棒。
但是!!我又想
我每次执行完一个异步操作并且获取到数据之后,都要手动调用next方法并传入数据,让程序继续执行下去。太麻烦了吧!
那我手写一个让迭代器自动执行并且获取到最后数据的方法吧:
function co(iterator){
return new Promise((resolve,reject)=>{
function _next(v){
const { value, done } = iterator.next(v);
if(done){
return resolve(value)
}
Promise.resolve(value).then((res)=>{
_next(res)
},(reason)=>{
return reject(reason)
})
}
_next()
})
}
co(gen()).then((res)=>{
console.log(999,res)
})
终极Boss:async await
于是,我们的终极Boss出现了,他就是async 和 await。
其实就是上面的co函数以及生成器函数的语法糖。
async function fun(){
const result1 = await getFile();
const result2 = await getFile(result1);
const result3 = await getFile(result2);
}
与上面的生成器函数对比,可以发现,就是把*换成async,yield换成await
但是,async await还做了以下优化:
1.可自动执行next函数,不用手动执行。
2.await后面跟promise,如果不是promise,就调用Promise.resolve转换成Promise
3.await可以将promise的值返回
4.可以用同步的方式处理错误,可以在外层用try catch包裹所有的await。
以上,完全实现了互相依赖异步操作的串行代码化。真正的语法糖贼甜。