问题的起源
最近两天被这个异步给坑了好久,这里总结一下Javascript的异步操作。
一切都源于下面这样结构的一段代码:
function main(){console.log('test result: ' + test());}const somefunc = (array, callback) => {//the array is not used//call the callbacksetTimeout(() => {callback();}, 2000);}function test(){let res = 'default';let a = 1, b = 2;somefunc({},()=>{if(a > b){res = 'error';}else {res = 'warning';}});return res;}main();
你认为
test()
执行完res是什么?结果不是error
,也不是warning
,而是default
,我也是服气了,后来才搞明白这就是所谓的none-blocking
IO模型。就是说somefunc()
还没有执行完,test()
就return
了,相当于开启了一个子进程去做somefunc
的事情,使得二者并行进行了。然而JS是单线程的,因此就用了这种奇怪的异步的方式来进行。所以我们要让它像同步代码的顺序一样执行的话,需要怎么办呢?
更简单的例子
上面的例子还是比较复杂了,下面举个更简单的例子:
const printLetter = letter => {setTimeout(() => {console.log(letter);}, getRandom(1000,3000));};const getRandom = (max,min) => {return Math.floor(Math.random() * (max - min)) + min;};printLetter('A');printLetter('B');printLetter('C');
这样三个printLetter的操作,怎样保证结果是按照
A、B、C
的顺序输出呢?Callback
最传统的方案就是
callback
了,你问我callback
是什么?正如其名,回调函数,就是你让A
执行完毕再调用B
,B
执行完毕咋爱调用C
不就完成了这个操作了?于是代码就变成了这样:const printLetter = (letter,callback) => {setTimeout(() => {console.log(letter);callback();}, getRandom(1000,3000));};const getRandom = (max,min) => {return Math.floor(Math.random() * (max - min)) + min;};printLetter('A',()=>{printLetter('B',() => {printLetter('C',()=>{});});});
功能倒是实现了,但是怎么看怎么别扭,这么简单个功能都要搞成这个样子,稍微复杂些岂不是很要命了?
这个时候我们就要看看
ES6
带来的法宝了Promise
ES6
引入了Promise
的机制,什么是Promise
?基本上就是给小朋友说等下有糖吃哦,然后看小朋友把作业完成了,很乖,就给他糖吃,给吃糖就是resolve
,如果小朋友不写作业那当然你也可以reject
就没有糖了,使用Promise.then()
基本上就可以保证执行的顺序了,于是代码就变成了下面这样:const printLetter = letter => {return new Promise(resolve => {setTimeout(() => {console.log(letter);resolve();}, getRandom(1000,3000));});};const getRandom = (max,min) => {return Math.floor(Math.random() * (max - min)) + min;};printLetter('A').then(() => {printLetter('B').then(() => {printLetter('C');});});
好像有些逻辑了的样子,但是怎么还是怎么看怎么别扭啊??别急,我们还有
ES2017
的神器async
和await
Async Await
ES2017
给我们带来了神奇的async
和await
,它等于给标明async
的自动加隐式Promise
,即return
的东西自动给你包在一个Promise
里面,而await
则自动从Promise
里面取出结果,所以用async
和await
来写,上面的代码就变成了:const printLetter = async letter => {setTimeout(() => {console.log(letter);}, getRandom(1000,3000));};const getRandom = (max,min) => {return Math.floor(Math.random() * (max - min)) + min;};(async () => {await printLetter('A');await printLetter('B');await printLetter('C');})();
嗯,好多了,这里由于
await
只能在async
里面使用,因此我在这里用了一个IIFE
即Immediately Invoked Function Expression
,立即执行的函数,来把它包装起来,这样就大功告成了。不得不说async
和await
使生活更加轻松,大家能用async
和await
尽量用async
和await
吧,不要再掉入callback hell
里面去了。总体来说JS的这一特性还是值得称赞的,但是跟C#
的Async
和Await
一比的话,还是有一点Naive
,毕竟这个方式就是从C#
借鉴而来,不过鉴于JS超强的跨平台兼容性,也算是各有所长啦。解决方案
回到上面的问题,其中一个关键的问题是
somefunc
是一个第三方库,我们没法修改的,那怎么解决这个问题呢?答案是使用Node.js
提供的神器Promisfy
,它能把传统的Callback
和Promise
的function
统统变成async
和await
的形式,这样就再也不用头疼了,于是乎上述代码就变成了:const util = require('util');async function main(){console.log('test result: ' + await test());}const somefunc = (array, callback) => {//the array is not used//call the callbacksetTimeout(() => {return callback();}, 2000);}async function test(){let res = 'default';let a = 1, b = 2;await util.promisify(somefunc).bind(this)({});if(a > b){return 'error';}else {return 'warning';}}main();
这一次代码终于按照正确逻辑执行了,得到了
res='warning'
的结果。