上一篇我们梳理了koa中中间件洋葱模型的执行原理,实现了一个进程管理功能,让洋葱模型自动运行。本篇我们来研究一下koa中异步回调的同步编写原理。同样,我们也会实现一个管理功能。是的,我们可以通过同步写法来写异步回调函数。一、回调金字塔及理想方案我们都知道javascript是一种单线程异步非阻塞语言。异步非阻塞当然是它的优点之一,但是大量的异步操作必然涉及到大量的回调函数,尤其是异步嵌套的时候,会出现回调金字塔问题,使得代码的可读性非常差。例如下面的例子:varfs=require('fs');fs.readFile('./file1',function(err,data){console.log(data.toString());fs.readFile('./file2',function(err,data){console.log(data.toString());})})这个例子是依次读取并打印两个文件的内容,而file2的读取必须在file1的读取完成之后再进行,所以它的操作必须在file1读取的回调函数。这是典型的回调嵌套,只有两层。在实际编程中,我们可能会遇到更多层的嵌套。这样的代码写法无疑不够优雅。在我们的想象中,更优雅的写法应该是看似同步实则异步的写法,类似如下:vardata;data=readFile('./file1');//下面的代码是第一个readFile执行的回调partaftercompletionconsole.log(data.toString());//下面的代码是第二次readFile的回调data=readFile('./file2');console.log(data.toString());这种写法完全避免了回调地狱。其实koa允许我们这样写异步回调函数:varkoa=require('koa');varapp=koa();varrequest=require('somemodule');app.use(function*(){vardata=yieldrequest('http://www.baidu.com');//下面是异步回调部分this.body=data.toString();})app.listen(3000);那么,是什么让koa如此神奇?2、Generator配合Promise实现异步回调和同步写入的重点。其实在上一篇文章中提到,generator有一个类似“断点”的作用。当遇到yield时,它会暂停,将控制权交给yield后面的函数,下次返回时继续执行。在上面的koa示例中,yield之后没有任何对象就可以了!必须是特定类型。在co函数中,可以支持promise、thunk函数等。今天的文章,我们就以promise为例来分析看看如何使用generator和promise配合实现异步同步。还是以第一个读取文件的例子来分析。首先,我们需要将读取文件的功能进行改造,封装成一个promise对象:readFile(fileName,function(err,data){if(err){reject(err);}else{resolve(data);}})})}//下面是readFile的用法示例vartmp=readFile('./file1');tmp.then(function(data){console.log(data.toString());})关于promise的使用,不熟悉的可以去看里面的语法es6。(我近期也会写一篇文章,教大家如何用es5语法实现一个具有基本功能的promise对象,敬请期待^_^)简单来说,promise可以通过promise.then(callback)来实现回调函数)形式来写。但我们的目标是配合生成器,真正实现如丝般顺滑的同步写入。如何合作?看这段代码:varfs=require('fs');varreadFile=function(fileName){returnnewPromise(function(resolve,reject){fs.readFile(fileName,function(err,data){if(err){reject(err);}else{resolve(data);}})})}//将读取文件的过程放在生成器中vargen=function*(){vardata=yieldreadFile('./文件1');console.log(data.toString());data=yieldreadFile('./file2');console.log(data.toString());}//手动执行generatorvarg=gen();varanother=g.next();//another.value为返回的promise对象another.value.then(function(data){//再次调用g.next从断点开始执行生成器,并将数据作为参数传回varanother2=g.next(data);another2.value.then(function(data){g.next(data);})})上面代码中,我们在生成器中yieldreadFile,回调语句代码写在yield之后的代码中,完全同步,实现了文章开头的思路。yield之后我们得到的是another.value是一个promise对象。我们可以使用then语句来定义回调函数。函数内容是将读取到的数据返回给生成器,继续让生成器从断点处开始执行。基本上,这就是异步回调同步的核心原理。其实熟悉python的人就会知道,python中有一个“协程”的概念,基本上是使用generator来实现的(我觉得es6的generator是用来参考Python的~)但是,我们还是手动执行上面的代码。然后和上一篇一样,我们还需要实现一个run函数来管理generator进程,让它自动运行起来!3、让同步回调函数自动运行:写run函数的时候,仔细观察前面代码中手动执行生成器的部分,也能发现一个规律。这个规则允许我们直接写一个递归函数:varrun=function(gen){v??arg;if(typeofgen.next==='function'){g=gen;}else{g=gen();}functionnext(数据){vartmp=g.next(数据);如果(tmp.完成){返回;}else{tmp.value.then(next);}}next();}函数接收一个生成器并使其中的异步自动执行。使用这个运行函数,让我们让前面的异步代码自动执行:varfs=require('fs');varrun=function(gen){varg;if(typeofgen.next==='function'){g=gen;}else{g=gen();}functionnext(data){vartmp=g.next(data);如果(tmp.done){返回;}else{tmp.value.then(next);}}next();}varreadFile=function(fileName){returnnewPromise(function(resolve,reject){fs.readFile(fileName,function(err,data){if(err){reject(err);}else{resolve(data);}})})}//把读取文件的过程放到生成器中vargen=function*(){vardata=yieldreadFile('./file1');console.log(data.toString());data=yieldreadFile('./file2');console.log(data.toString());}//下面只需要将gen放入run即可自动执行run(gen);执行上面的代码,可以看到终端依次打印出了file1和file2的内容。需要指出的是这里的run函数为简单起见只支持promise,而实际的co函数还支持thunk等。这样co函数的两个功能就基本介绍完了,一个是洋葱模型的流程控制,一个是异步和同步代码的自动执行。下一篇文章,我将带大家将这两个函数整合起来,写出我们自己的co函数!本文的代码也可以在github上找到:https://github.com/mly-zju/async-js-demo,其中promise_generator.js是本文的示例源码。也欢迎大家多多关注我的githubpages个人博客,我会不定时更新我的??技术文章~
