当前位置: 首页 > news >正文

深入探析koa之异步回调处理篇


在上一篇中我们梳理了koa当中中间件的洋葱模型执行原理,并实现了一个可以让洋葱模型自动跑起来的流程管理函数。这一篇,我们再来研究一下koa当中异步回调同步化写法的原理,同样的,我们也会实现一个管理函数,是的我们能够通过同步化的写法来写异步回调函数。

1. 回调金字塔及理想中的解决方案

我们都知道javascript是一门单线程异步非阻塞语言。异步非阻塞当然是它的一个优点,但大量的异步操作必然涉及大量的回调函数,特别是当异步嵌套的时候,就会出现回调金字塔的问题,使得代码的可读性非常差。比如下面一个例子:

 
 
  1. var fs = require('fs'); 
  2.  
  3. fs.readFile('./file1'function(err, data) { 
  4.   console.log(data.toString()); 
  5.   fs.readFile('./file2'function(err, data) { 
  6.     console.log(data.toString()); 
  7.   }) 
  8. })  

这个例子是先后读取两个文件内容并打印,其中file2的读取必须在file1读取结束之后再进行,因此其操作必须要在file1读取的回调函数中执行。这是一个典型的回调嵌套,并且只有两层而已,在实际编程中,我们可能会遇到更多层的嵌套,这样的代码写法无疑是不够优雅的。

在我们想象中,比较优雅的一种写法应该是看似同步实则异步的写法,类似下面这样:

 
 
  1. var data; 
  2. data = readFile('./file1'); 
  3. //下面的代码是第一个readFile执行完毕之后的回调部分 
  4. console.log(data.toString()); 
  5. //下面的代码是第二个readFile的回调 
  6. data = readFile('./file2'); 
  7. console.log(data.toString());  

这样的写法,就完全避免回调地狱。事实上,koa就让我们可以使用这样的写法来写异步回调函数:

 
 
  1. var koa = require('koa'); 
  2. var app = koa(); 
  3. var request=require('some module'); 
  4.  
  5. app.use(function*() { 
  6.   var data = yield request('http://www.baidu.com'); 
  7.   //以下是异步回调部分 
  8.   this.body = data.toString(); 
  9. }) 
  10.  
  11. app.listen(3000); 

那么,究竟是什么让koa有这么神奇的魔力呢?

2. generator配合promise实现异步回调同步写法

关键的一点,其实前一篇也提到了,就是generator具有类似"打断点"这样的效果。当遇到yield的时候,就会暂停,将控制权交给yield后面的函数,当下次返回的时候,再继续执行。

而在上面的那个koa例子中,yield后面的可不是任何对象都可以哦!必须是特定类型。在co函数中,可以支持promise, thunk函数等。

今天的文章中,我们就以promise为例来进行分析,看看如何使用generator和promise配合,实现异步同步化。

依旧以第一个读取文件例子来分析。首先,我们需要将读文件的函数进行改造,将其封装成为一个promise对象:

 
 
  1. var fs = require('fs'); 
  2.  
  3. var readFile = function(fileName) { 
  4.   return new Promise(function(resolve, reject) { 
  5.     fs.readFile(fileName, function(err, data) { 
  6.       if (err) { 
  7.         reject(err); 
  8.       } else { 
  9.         resolve(data); 
  10.       } 
  11.     }) 
  12.   }) 
  13.  
  14. //下面是readFile使用的示例 
  15. var tmp = readFile('./file1'); 
  16. tmp.then(function(data) { 
  17.   console.log(data.toString()); 
  18. })  

关于promise的使用,如果不熟悉的可以去看看es6中的语法。(近期我也会写一篇文章来教大家如何用es5的语法来自己实现一个具备基本功能的promise对象,敬请期待呦^_^)

简单来讲,promise可以实现将回调函数通过 promise.then(callback)的形式来写。但是我们的目标是配合generator,真正实现如丝般顺滑的同步化写法,如何配合呢,看这段代码:

 
 
  1. var fs = require('fs'); 
  2.  
  3. var readFile = function(fileName) { 
  4.   return new Promise(function(resolve, reject) { 
  5.     fs.readFile(fileName, function(err, data) { 
  6.       if (err) { 
  7.         reject(err); 
  8.       } else { 
  9.         resolve(data); 
  10.       } 
  11.     }) 
  12.   }) 
  13.  
  14. //将读文件的过程放在generator中 
  15. var gen = function*() { 
  16.   var data = yield readFile('./file1'); 
  17.   console.log(data.toString()); 
  18.   data = yield readFile('./file2'); 
  19.   console.log(data.toString()); 
  20.  
  21. //手动执行generator 
  22. var g = gen(); 
  23. var another = g.next(); 
  24. //another.value就是返回的promise对象 
  25. another.value.then(function(data) { 
  26.   //再次调用g.next从断点处执行generator,并将data作为参数传回 
  27.   var another2 = g.next(data); 
  28.   another2.value.then(function(data) { 
  29.     g.next(data); 
  30.   }) 
  31. })  

上述代码中,我们在generator中yield了readFile,回调语句代码写在yield之后的代码中,完全是同步的写法,实现了文章一开头的设想。

而yield之后,我们得到的是一个another.value是一个promise对象,我们可以使用then语句定义回调函数,函数的内容呢,则是将读取到的data返回给generator并继续让generator从断点处执行。

基本上这就是异步回调同步化最核心的原理,事实上如果大家熟悉python,会知道python中有"协程"的概念,基本上也是使用generator来实现的(我想当怀疑es6的generator就是借鉴了python~)

不过呢,上述代码我们依然是手动执行的。那么同上一篇一样,我们还需要实现一个run函数,用于管理generator的流程,让它能够自动跑起来!

3. 让同步化回调函数自动跑起来:一个run函数的编写

仔细观察上一段代码中手动执行generator的部分,也能发现一个规律,这个规律让我们可以直接写一个递归的函数来代替:

 
 
  1. var run=function(gen){ 
  2.   var g; 
  3.   if(typeof gen.next==='function'){ 
  4.     g=gen; 
  5.   }else
  6.     g=gen(); 
  7.   } 
  8.  
  9.   function next(data){ 
  10.     var tmp=g.next(data); 
  11.     if(tmp.done){ 
  12.       return ; 
  13.     }else
  14.       tmp.value.then(next); 
  15.     } 
  16.   } 
  17.  
  18.   next(); 
  19. }  

函数接收一个generator,并让其中的异步能够自动执行。使用这个run函数,我们来让上一个异步代码自动执行:

 
 
  1. var fs = require('fs'); 
  2.  
  3. var run = function(gen) { 
  4.   var g; 
  5.   if (typeof gen.next === 'function') { 
  6.     g = gen; 
  7.   } else { 
  8.     g = gen(); 
  9.   } 
  10.  
  11.   function next(data) { 
  12.     var tmp = g.next(data); 
  13.     if (tmp.done) { 
  14.       return
  15.     } else { 
  16.       tmp.value.then(next); 
  17.     } 
  18.   } 
  19.  
  20.   next(); 
  21.  
  22. var readFile = function(fileName) { 
  23.   return new Promise(function(resolve, reject) { 
  24.     fs.readFile(fileName, function(err, data) { 
  25.       if (err) { 
  26.         reject(err); 
  27.       } else { 
  28.         resolve(data); 
  29.       } 
  30.     }) 
  31.   }) 
  32.  
  33. //将读文件的过程放在generator中 
  34. var gen = function*() { 
  35.   var data = yield readFile('./file1'); 
  36.   console.log(data.toString()); 
  37.   data = yield readFile('./file2'); 
  38.   console.log(data.toString()); 
  39. //下面只需要将gen放入run当中即可自动执行 
  40. run(gen);  

执行上述代码,即可看到终端依次打印出了file1和file2的内容。

需要指出的是,这里的run函数为了简单起见只支持promise,而实际的co函数还支持thunk等。

这样一来,co函数的两大功能基本就完整介绍了,一个是洋葱模型的流程控制,另一个是异步同步化代码的自动执行。在下一篇文章中,我将带大家对这两个功能进行整合,写出我们自己的一个co函数!


作者:勇敢的半导体

来源:51CTO

相关文章:

  • 方法和数组
  • 物联网网络技术市场的发展
  • centos中安装mysql
  • 日志收集+分析+报警 logstash
  • 请问一下发博中出现的问题
  • Educational Codeforces Round 26
  • Android 带你撸一个好玩的 DoodleView(涂鸦)
  • javascript中的this和e.target的深入研究
  • 2017-08-04 前端日报
  • 二叉树的最小深度 Minimum Depth of Binary Tree
  • 告别ASP.NET操作EXCEL的烦恼(总结篇)
  • 一个简单RPC框架是怎样炼成的(VI)——引入服务注冊机制
  • UVa 123042D Geometry 110 in 1! [平面几何]
  • 【实用代码片段】将json数据绑定到html元素 (转)
  • HNUSTOJ 1516:Loky的烦恼
  • 【干货分享】SpringCloud微服务架构分布式组件如何共享session对象
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • 11111111
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • create-react-app项目添加less配置
  • input实现文字超出省略号功能
  • iOS 颜色设置看我就够了
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • Java多态
  • Laravel5.4 Queues队列学习
  • Redux系列x:源码分析
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • 给Prometheus造假数据的方法
  • 每天一个设计模式之命令模式
  • 配置 PM2 实现代码自动发布
  • 悄悄地说一个bug
  • 使用Swoole加速Laravel(正式环境中)
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • 怎么将电脑中的声音录制成WAV格式
  • Spring Batch JSON 支持
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • #Z0458. 树的中心2
  • (14)Hive调优——合并小文件
  • (4)Elastix图像配准:3D图像
  • (4)logging(日志模块)
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (附源码)spring boot建达集团公司平台 毕业设计 141538
  • (十) 初识 Docker file
  • (十八)用JAVA编写MP3解码器——迷你播放器
  • (十三)Flask之特殊装饰器详解
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • .net/c# memcached 获取所有缓存键(keys)
  • .NetCore Flurl.Http 升级到4.0后 https 无法建立SSL连接
  • .NetCore项目nginx发布
  • .net解析传过来的xml_DOM4J解析XML文件
  • .net专家(高海东的专栏)
  • .Net转Java自学之路—基础巩固篇十三(集合)
  • @synthesize和@dynamic分别有什么作用?