深入解析js异步编程利器Generator
我们在编写Nodejs程序时,经常会用到回调函数,在一个操作执行完成之后对返回的数据进行处理,我简单的理解它为异步编程。
如果操作很多,那么回调的嵌套就会必不可少,那么如果操作非常多,那么回调的嵌套就会变得让人无法忍受了。
我们知道的Promises就是问了解决这个问题而提出来的。然而,promises并不是一种新的功能,它只是一种新的写法,原来横向发展的回调函数,被排成了队竖向发展。
然而,Generator不同,它是一种新的解决方案。
文章中提到的所有代码都可以在这里找到源码:[查看源码]。
文章目录:
- 一、ES6转码器
- 二、Generator函数和yield关键字的含义
- 三、为什么要使用Generator函数?
- 四、Koa中的Generator
- 五、关于Generator容易误解的一点
- 六、Generator的闪光点
- 七、拓展阅读
一、ES6转码器
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
虽然现在各个浏览器对ES6的支持度已经越来越高,但是始终不完整,同时也为了兼容不同版本的浏览器实现,于是在一些情况下我们可能需要借助于一些ES6的转码器将ES6代码转化成ES5标准的代码。
1.1 babeljs-online
在边理解ES6,边写一些ES6相关的代码片段时,我喜欢用[babeljs-online]。用它我们可以不用在文本编辑器和命令行控制台之间来回切换,就可以写并且实时运行ES6的代码了。
它是长成这个样子的:

左边可以直接写ES6语法的代码,左下角提供实时的错误检测;
右边是实时编译成的ES5语法的代码,右下角可以输出console.log();
1.2 babel-node
当然我们也可以安装babel模块:
1 npm install --global babel
2 babel-node
这样就像在REPL中一样,我们可以写然后执行ES6代码了:

不过babel-node现在还不支持多行输入。
1.3 Traceur在线编辑器
Traceur同样是一个在线编辑器,在线的将ES6的代码转为ES5的代码。

我比较喜欢babel的在线编辑器,有实时的语法错误提醒和运行结果的输出。一般情况下我选择它写一些代码片段。
二、Generator函数和yield关键字的含义
通过一些简单的代码片段,我们来看一下Generator的含义和其中的一些要点。可以在[babeljs-online]中自行键入代码理解其含义。
这里有一个简单的例子:
查看运行结果:[helloGenerator代码片段]
2.1 yield语句的执行会暂停当前函数的执行并保存当前的堆栈,返回当前yield语句的值。
2.2 Generator函数与普通的函数不同,它只定义了遍历器,不会执行,每次调用这个遍历器的next方法,就从函数体的头部或者上一次停下来的地方开始执行,直到下一个yield语句为止。
yield语句就是暂停的标志,next方法遇到yield,就会暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value属性值。当下一次调用next方法时,再继续向下执行,知道遇到下一个yield语句。如果没有再遇到新的yield语句,就一直运行到函数结束,将return语句后面的表达式的值,作为value属性的值,如果该函数没有return语句,则value属性的值为undefined。
2.3 如果Generator函数不使用yield语句,这是它就变成了一个单纯地暂缓执行函数:
查看运行结果:[暂缓执行的Generator函数]
2.4 yield语句不能用在普通函数中,否则会报错:
查看运行结果:[在普通函数中使用yield语句报错的例子]
可以看到编辑器报错了。
2.5 yield语句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当做上一个yield语句的返回值。但是由于next方法的参数表示上一个yield语句的返回值,所以第一次使用next方法时,不能带有参数,V8会直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。
2.6 for...of循环可以自动遍历Generator函数,并且此时不需要再调用next方法:
上面的代码会输出1,2,3,4并不会输出5,这是因为一旦next方法返回的对象的done属性为true,forof循环就会停止。
2.7 如果yield语句后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器,这又叫yield*语句。
可以看下这个例子:
查看运行结果:[yield*语句]
其实yield*语句就等同于在Generator函数内部部署一个for...of循环。
更多关于Generator函数的细节,严重推荐阅读阮一峰老师的ES6入门:[ES6 入门 - Generator]
三、为什么要使用Generator函数?
虽然我们认识了Generator函数,知道它的含义,怎么使用。但是,为什么要使用Generator函数呢?它解决了什么问题?有什么优势?
我们从一个简单的[查找当前目录下得最大文件]的node程序开始,使用不同的解决方案来解决callback问题。
我们想写一个模块,然后在入口文件中调用它找到当前目录下的最大文件:
1 var findLargest = require('./readmaxnested');
2
3
4 //获取当前根目录下的最大的文件名称
5 findLargest('./',function(err,filename){
6 if(err) return console.log(err);
7 console.log('largest file was:', filename);
8 });
3.1 嵌套的解决方案
最先可以想到的方案:
1.读取当前文件夹下的所有文件;
2.获取到每个文件的stat,在确认IO操作都完成了的情况下比较文件大小;
3.过滤掉目录等 只比较文件;
4.通过callback返回最大的文件的filename。
实现起来应该是这个样子的:
再正常和正确不过的方案了,但是其中会有一些问题。可以看到我们通过counter变量用于确保所有的IO操作都已完成时才开始文件的比较;通过errored变量来确保出错时只调用一次错误回掉。这样可以看到上面的这段程序在管理并行的操作的时候需要额外的小心。
3.2 模块化的解决方案
为了让代码的重用和测试变得简单些,我们可以稍稍改进下代码,提取出可充用的函数和方法:
然而这还没有做出一些实质性的改进,可以看到,我们还是通过两个变量手动的管理了程序的流程。
3.3 使用async模块的解决办法
我们使用很流行的async模块来改写下我们的程序:
这里比较关键的两个方法:
1.async.waterfall,它提供一系列的执行流程,通过一系列的回调data可以从一个函数传递到下一个函数;
2.async.map,可以并行的执行fs.stat然后返回一个结果数组;
可以看到async只提供了一个callback,我们不用在担心流程的控制和回调函数被调用的次数了。
3.4 使用promises的解决方案
promises是为了结局多重嵌套的回调而提出的一种解决办法。它不是新的语法功能,只是一种新的写法。它允许将回调函数的横向加载改成纵向加载。
我们来用promises写法改写下代码:
Q.all将会并行的获取到所有文件的stats并且返回一个数组。
其实代码一眼看上去是有些冗余的,原来的任务被promises包装了一下,看上去是好多的then,语义变得有些不清楚。
更多的关于promises和Q模块可以阅读:[Promises in nodejs with Q]
3.5 Generator的解决方案
我们使用ES6的Generator这个异步编程的解决方案,来改进代码:
这里使用了co模块和Thunk函数,后面会有详细的解释,这里先跳过。
我们可以看到几行代码精巧的解决了问题,并且语义清晰,同时回调的问题也不复存在。
我们看到了使用co封装了Generator的更加优雅和简单的异步编程方式。
可以在这里查看上述所有代码:[查找当前目录下的最大文件]
四、Koa中的Generator
从上面的最后一个代码片段中我们可以看出,这里的Generator与原生的Generator 有一定的不同,因为koa中的Generator使用了co进行了封装。
4.1 co的简单使用:
1 var co = require('co');
2 var fs = require('fs');
3
4 function read(file) {
5 return function(fn){
6 fs.readFile(file, 'utf8', fn);
7 }
8 }
9 co(function *(){
10
11 var a = yield read('.gitignore');
12 console.log(a.length);
13
14 var b = yield read('package.json');
15 console.log(b.length);
16 });
co要求所有的异步函数都是thunk函数:
1 function read(file) {
2 return function(fn){
3 fs.readFile(file, 'utf8', fn);
4 }
5 }
如果需要对thunk函数返回的数据做一些处理可以写在回调函数中:
1 function read(file) {
2 return function(fn){
3 fs.readFile(file, 'utf8', function(err,result){
4 if (err) return fn(err);
5 fn(null, result);
6 });
7 }
8 }
我们也可以不用自己写thunk函数,使用thunkify模块就好了:
1 var thunkify = require('thunkify');
2 var fs = require('fs');
3
4 var read = thunkify(fs.readFile);
获取thunk函数的返回结果,就是用yield关键字就可以了:
1 var a = yield read('.gitignore');
2 console.log(a.length);
可以看到我们不用再使用next方法了,co将generator function的流转封装好了。
4.2 Thunk函数
我们将参数放在一个临时函数中,再将这个临时函数传入函数体,当用到该参数时对临时函数求值即可。这个临时函数就叫Thunk函数。
Thunk是“传名调用”的一种实现策略,用来替换某个表达式:
1 function f(m){
2 return m * 2;
3 }
4
5 f(x + 5);
6
7 // 等同于
8
9 var thunk = function () {
10 return x + 5;
11 };
12
13 function f(thunk){
14 return thunk() * 2;
15 }
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数:
1 // 正常版本的readFile(多参数版本)
2 fs.readFile(fileName, callback);
3
4 // Thunk版本的readFile(单参数版本)
5 var readFileThunk = Thunk(fileName);
6 readFileThunk(callback);
7
8 var Thunk = function (fileName){
9 return function (callback){
10 return fs.readFile(fileName, callback);
11 };
12 };
更多详细请阅读:[Thunk函数的含义和用法]
五、关于Generator容易误解的一点
现在看起来所有的Generator函数都在运行,我们使用yield关键字只是让它暂停了一下。那么难道这是传说中的多线程在js中的实现吗?
然而并不是的.. 我们只要牢牢的记住,js真的是单线程的。我们只是具备了在一个函数中暂停的能力。
并且,如果在对性能要求很高的情况下,generator可能不适合作为首选。
我们可以跑一个耗费CPU的例子来看一下。
使用普通的函数和Generator函数分别来实现斐波那契数列的计算:
var suite = new (require('benchmark')).Suite;
function fib(n){
var current = 0,next = 1,swap;
for(var i=0;i<n;i++){
swap = current,current = next;
next = swap + next;
}
return current;
}
function* fibGen(n){
var current = 0,next = 1,swap;
for(var i=0;i<n;i++){
swap = current,current = next;
next = swap + next;
yield current;
}
}
suite
.add('regular',function(){
fib(20);
})
.add('generator',function(){
for(var n of fibGen(20));
})
.on('complete',function(){
console.log('results:');
this.forEach(function(result){
console.log(result.name,result.count);
})
})
.run()
这里我们使用了benchmark模块,来测量执行时间。
然后我们执行:
node --harmony fibonacci.js
就可以查看到执行的结果:
results:
regular 1434583
generator 39962
可以看到generator的效率并不比普通的函数要好(这里面数值越大越好)。
六、Generator的闪光点
我们看了这么多generator的东西,写了一些例子,那么它的闪光点到底在那些方面呢? 我们来总结一下。
6.1 懒惰的执行
也就是传说中的lazy evaluation。其实我们通过闭包也可以实现,但是通过使用yield使事情变得简单了。我们可以在我们需要获取相应的值得时候才去获取:
var fibIter = fibGen(20)
var next = fibIter.next()
console.log(next.value) setTimeout(function () {
var next = fibIter.next()
console.log(next.value)
},2000)
如果我们想从头到尾的执行也可使用上面说到的for of循环:
for (var n of fibGen(20) {
console.log(n)
}
6.2 无限的执行
既然generator函数的执行是懒惰的,在我们需要的时候去执行获取相应的值就可以了,那么就可能实现无限执行的情况:
function* fibGen () {
var current = 0, next = 1, swap
while (true) {
swap = current, current = next
next = swap + next
yield current
}
}
6.3 同步的控制流
最初使generator用于同步控制流程的是task.js(现在已经不复存在)。这个概念因为co模块和其他一些promise实现而变得流行了起来。
但是在如何实现呢?
在Node中,我们总是把一些事情都放到回调里去做,来时先同步的效果,这可能是比较初级的同步实现。前面也有例子,一步一步的进化成简介高效的generator版本。
这里有个简单的例子,使我们得到了同步的版本,但是写起来就像异步实现的一样:
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
run(function* (){
try{
var file = yield readFile('./fibonacci.js');
console.log(file.toString());
}catch (er){
console.error(er);
}
});
function run(genFn){
var gen = genFn();
next();
function next(er,value){
if(er) return gen.throw(er);
var continuable = gen.next(value);
if(continuable.done) return;
var cbFn = continuable.value;
cbFn(next);
}
}
文章中提到的所有代码都可以在这里找到源码:[查看源码]。
七、拓展阅读:
[sync 模块]
[async函数的含义和用法]
深入解析js异步编程利器Generator的更多相关文章
- JS异步编程 (2) - Promise、Generator、async/await
JS异步编程 (2) - Promise.Generator.async/await 上篇文章我们讲了下JS异步编程的相关知识,比如什么是异步,为什么要使用异步编程以及在浏览器中JS如何实现异步的.最 ...
- 一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async
JS异步编程实践理解 回顾JS异步编程方法的发展,主要有以下几种方式: Callback Promise Generator Async 需求 显示购物车商品列表的页面,用户可以勾选想要删除商品(单选 ...
- JS异步编程 (1)
JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTi ...
- JS魔法堂:深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- 深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- node.js异步编程的几种模式
Node.js异步编程的几种模式 以读取文件为例: 1.callback function const fs = require('fs'); //callback function fs.readF ...
- js异步编程
前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的, ...
- 前端分享----JS异步编程+ES6箭头函数
前端分享----JS异步编程+ES6箭头函数 ##概述Javascript语言的执行环境是"单线程"(single thread).所谓"单线程",就是指一次只 ...
- node.js异步编程解决方案之Promise用法
node.js异步编程解决方案之Promise var dbBase = require('../db/db_base'); var school_info_db = require('../db/s ...
随机推荐
- 【小程序分享篇 一 】开发了个JAVA小程序, 用于清除内存卡或者U盘里的垃圾文件非常有用
有一种场景, 手机内存卡空间被用光了,但又不知道哪个文件占用了太大,一个个文件夹去找又太麻烦,所以我开发了个小程序把手机所有文件(包括路径下所有层次子文件夹下的文件)进行一个排序,这样你就可以找出哪个 ...
- boost强分类器的实现
boost.cpp文件下: bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator, int _numSampl ...
- C# 发送邮件 附件名称为空
示例代码: // 1.创建邮件 MailMessage mailMsg = new MailMessage(); mailMsg.To.Add(new MailAddress("test@ ...
- 在Ubuntu 16.10安装mysql workbench报未安装软件包 libpng12-0错误
1.安装mysql workbench,提示未安装软件包 libpng12-0 下载了MySQL Workbench 6.3.8 在安装的时候报错: -1ubu1604-amd64.deb 提示: ...
- 使用C/C++写Python模块
最近看开源项目时学习了一下用C/C++写python模块,顺便把学习进行一下总结,废话少说直接开始: 环境:windows.python2.78.VS2010或MingW 1 创建VC工程 (1) 打 ...
- 一个技术汪的开源梦 —— 公共组件缓存之分布式缓存 Redis 实现篇
Redis 安装 & 配置 本测试环境将在 CentOS 7 x64 上安装最新版本的 Redis. 1. 运行以下命令安装 Redis $ wget http://download.redi ...
- 神技!微信小程序(应用号)抢先入门教程(附最新案例DEMO-豆瓣电影)持续更新
微信小程序 Demo(豆瓣电影) 由于时间的关系,没有办法写一个完整的说明,后续配合一些视频资料,请持续关注 官方文档:https://mp.weixin.qq.com/debug/wxadoc/de ...
- Windos环境用Nginx配置反向代理和负载均衡
Windos环境用Nginx配置反向代理和负载均衡 引言:在前后端分离架构下,难免会遇到跨域问题.目前的解决方案大致有JSONP,反向代理,CORS这三种方式.JSONP兼容性良好,最大的缺点是只支持 ...
- js数组去重几种思路
在一些后台语言中都内置了一些方法来处理数组或集合中重复的数据.但是js中并没有类似的方法,网上已经有一些方法,但是不够详细.部分代码来源于网络.个人总计如下:大致有4种思路 1)使用两次循环比较原始的 ...
- Missing Push Notification Entitlement 问题
最近打包上传是遇到一个问题: 描述: Missing Push Notification Entitlement - Your app includes an API for Apple's Push ...