ES7之后JavaScript提供了多种异步编程的实现方式,接下来通过例子比较一下各种方式在使用上的不同。
我们将通过异步的方式调用下面Sleep函数
// sleep: number -> Promise<number>
function sleep(ms){
return new Promise(resolve =>
setTimeout((()=>resolve(ms)), ms));
}
1. async/await
async function main(){
let a = await sleep(1000);
alert(`${a}ms passed`); // 1000ms passed
let b = await sleep(2000);
alert(`${b}ms passed`); // 2000ms passed
let [c, d] = await Promise.all([
sleep(3000),
sleep(4000)
]);
alert(`${Math.max(c, d)}ms passed`); // 4000ms passed
alert('done');
}
main(); // return Promise
async是实现异步调用最简洁的方式,async内部可以通过await使一个异步调用转为同步。想要同步多个异步结果时,可以使用Promise.all配合await实现。
2. generator/yield
// main: void -> Promise<void>
let main = async(function* _main(){
let a = yield sleep(1000);
alert(`${a}ms passed`);
let b = yield sleep(2000);
alert(`${b}ms passed`);
let [c, d] = yield Promise.all([
sleep(3000),
sleep(4000)
]);
alert(`${Math.max(c, d)}ms passed`);
alert('done');
});
// async: (void -> Generator) -> (void -> Promise)
function async(generatorFunc) {
let generator = generatorFunc();
let onResolved = arg =>{
let result = generator.next(arg);
if (result.done) {
return result.value;
} else {
return result.value.then(onResolved);
}
}
return onResolved;
}
main(); // return Promise<void>
async函数接受一个generator函数返回Promise。generator函数_main内部调用的Promise成功后会调用next,进行后需处理,相当于用yield模仿了await的效果。整体上用generator/yield模拟了async/await的效果。
3. Promise/then
// main: void -> Promise<void>
function main(){
return sleep(1000).then((a)=>{
alert(`${a}ms passed`);
return sleep(2000).then((b)=>{
alert(`${b}ms passed`);
return Promise.all([sleep(3000), sleep(4000)]).then(([c, d])=>{
alert(`${Math.max(c, d)}ms passed`);
alert('done');
return;
});
});
});
});
main(); // return Promise<void>
使用Promise/then的问题是无法避免回调地狱,所以ES7出现async/await之后,以下这样的代码
function main(){
return getA().then((a)=>
getB().then((b)=>
a + b));
}
变更多的用下面这样的代码代替
async function main(){
const a = await getA();
const b = await getB();
return a + b;
}
4. CPS
首先实现CPS版本的sleep如下:
// sleep: number -> (number -> void) -> void
function sleep(ms, cb){
setTimeout((()=> cb(ms)), ms);
return;
}
然后实现异步调用如下:
// main: void -> Promise<void>
function main(){
sleep(1000, (a)=>{
alert(`${a}ms passed`);
sleep(2000, (b)=>{
alert(`${b}ms passed`);
let waitAll = genWaitAll(next);
sleep(3000, waitAll());
sleep(4000, waitAll());
function next([c, d]){
alert(`${Math.max(c, d)}ms passed`);
alert('done');
return;
}
});
});
};
// genWaitAll: ([a] -> void) -> (void -> (a -> void))
function genWaitAll(next){
let results = [];
let counter = 0
return ()=>{
let i = counter;
counter++;
return (ms)=>{
results[i] = ms;
counter--;
if (counter === 0){
next(results);
}
};
}
};
main(); // return void;
CPS本质就是利用回调实现异步编程,所以会出现回调地狱的问题,同时为了实现Promise.all的效果需要手动实现genWaitAll()
5. 总结
我们通过例子对比了JS各种异步编程的实现方案,需要说明的是,这些方案诞生的时间与我们的介绍是相反的,一句话总结他们之间的关系:CPS通过回调实现了最朴素的异步编程方案;ES6提出Promise,在CPS的基础上实现了then、Promise.all、Promise.race等功能、使用起来更加方便;generator可以帮助Promise中实现异步编程中的同步;ES7中出现的async/await相当于generator/yield的语法糖,可以更简单地实现异步编程中的同步。