深入解析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函数分别来实现斐波那契数列的计算:

 1 var suite = new (require(‘benchmark‘)).Suite;
 2
 3 function fib(n){
 4   var current = 0,next = 1,swap;
 5   for(var i=0;i<n;i++){
 6     swap = current,current = next;
 7     next = swap + next;
 8   }
 9   return current;
10 }
11
12 function* fibGen(n){
13   var current = 0,next = 1,swap;
14   for(var i=0;i<n;i++){
15     swap = current,current = next;
16     next = swap + next;
17     yield current;
18   }
19 }
20
21 suite
22   .add(‘regular‘,function(){
23     fib(20);
24   })
25   .add(‘generator‘,function(){
26     for(var n of fibGen(20));
27   })
28   .on(‘complete‘,function(){
29     console.log(‘results:‘);
30     this.forEach(function(result){
31       console.log(result.name,result.count);
32     })
33   })
34   .run()

这里我们使用了benchmark模块,来测量执行时间。

然后我们执行:

1 node --harmony fibonacci.js

就可以查看到执行的结果:

1 results:
2 regular 1434583
3 generator 39962

可以看到generator的效率并不比普通的函数要好(这里面数值越大越好)。

六、Generator的闪光点

我们看了这么多generator的东西,写了一些例子,那么它的闪光点到底在那些方面呢? 我们来总结一下。

6.1 懒惰的执行

也就是传说中的lazy evaluation。其实我们通过闭包也可以实现,但是通过使用yield使事情变得简单了。我们可以在我们需要获取相应的值得时候才去获取:

1 var fibIter = fibGen(20)
2 var next = fibIter.next()
3 console.log(next.value)
4
5 setTimeout(function () {
6  var next = fibIter.next()
7  console.log(next.value)
8 },2000)

如果我们想从头到尾的执行也可使用上面说到的for of循环:

1 for (var n of fibGen(20) {
2   console.log(n)
3 }

6.2 无限的执行

既然generator函数的执行是懒惰的,在我们需要的时候去执行获取相应的值就可以了,那么就可能实现无限执行的情况:

1 function* fibGen () {
2   var current = 0, next = 1, swap
3   while (true) {
4     swap = current, current = next
5     next = swap + next
6     yield current
7   }
8 }

6.3 同步的控制流

最初使generator用于同步控制流程的是task.js(现在已经不复存在)。这个概念因为co模块和其他一些promise实现而变得流行了起来。

但是在如何实现呢?

在Node中,我们总是把一些事情都放到回调里去做,来时先同步的效果,这可能是比较初级的同步实现。前面也有例子,一步一步的进化成简介高效的generator版本。

这里有个简单的例子,使我们得到了同步的版本,但是写起来就像异步实现的一样:

 1 var fs = require(‘fs‘);
 2 var thunkify = require(‘thunkify‘);
 3 var readFile = thunkify(fs.readFile);
 4
 5 run(function* (){
 6   try{
 7     var file = yield readFile(‘./fibonacci.js‘);
 8     console.log(file.toString());
 9   }catch (er){
10     console.error(er);
11   }
12 });
13
14 function run(genFn){
15   var gen = genFn();
16   next();
17   function next(er,value){
18     if(er) return gen.throw(er);
19     var continuable = gen.next(value);
20
21     if(continuable.done) return;
22     var cbFn = continuable.value;
23     cbFn(next);
24   }
25 }

文章中提到的所有代码都可以在这里找到源码:[查看源码]。

七、拓展阅读:

[ES6 Generator]

[sync 模块]

[async函数的含义和用法]

[Generator函数的含义和用法]

[Thunk函数的含义和用法]

[co函数的含义和用法]

[generators in good use]

时间: 2024-10-31 11:41:46

深入解析js异步编程利器Generator的相关文章

一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async

JS异步编程实践理解 回顾JS异步编程方法的发展,主要有以下几种方式: Callback Promise Generator Async 需求 显示购物车商品列表的页面,用户可以勾选想要删除商品(单选或多选),点击确认删除按钮后,将已勾选的商品清除购物车,页面显示剩余商品. 为了便于本文内容阐述,假设后端没有提供一个批量删除商品的接口,所以对用户选择的商品列表,需要逐个调用删除接口. 用一个定时器代表一次接口请求.那思路就是遍历存放用户已选择商品的id数组,逐个发起删除请求del,待全部删除完成

JS异步编程 (1)

JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTimeout(function cbFn(){ console.log('learnInPro'); }, 1000); console.log('sync things'); setTimeout就是一个异步任务,当JS引擎顺序执行到setTimeout的时候发现他是个异步任务,则会把这个任务挂起,

JS 异步编程六种方案

前言我们知道Javascript语言的执行环境是"单线程".也就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务. 这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行. 为了解决这个问题,Javascript语言将任务的执行模式分成两种

Javascript教程:js异步编程的4种方法详述(转载)

文章收集转载于(阮一峰的网络日志) 你可能知道,Javascript语言的执行环境是“单线程”(single thread). 所谓“单线程”,就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推. 这种模式的好处是实现起来比较简单,执行环境相对单纯:坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方

js异步编程

前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的,抽象起来有三种场景. 顺序做 先买菜,再买肉,最后洗米, 即 A->B->C. 并发做 买菜,买肉,洗米,一起做. 交集做 买菜,买肉必须先做完,才能做洗米. 场景就是这样,接下来就是如何考虑用js实现. function A(callback){ setTimeout(function(){ c

深入解析Javascript异步编程

这里深入探讨下Javascript的异步编程技术.(P.S. 本文较长,请准备好瓜子可乐 :D) 一. Javascript异步编程简介 至少在语言级别上,Javascript是单线程的,因此异步编程对其尤为重要. 拿nodejs来说,外壳是一层js语言,这是用户操作的层面,在这个层次上它是单线程运行的,也就是说我们不能像Java.Python这类语言在语言级别使用多线程能力.取而代之的是,nodejs编程中大量使用了异步编程技术,这是为了高效使用硬件,同时也可以不造成同步阻塞.不过nodejs

ES6 --- JS异步编程的几种解决方法及其优缺点

导言: 我们都知道 JS 是单线程的,这也正是异步编程对于 JS  很重要的原因,因为它无法忍受耗时太长的操作.正因如此有一系列的实现异步的方法. 方法一  setTimeout 常用于:定时器,动画效果 用法:setTimeout(func|code,delay) 缺点: setTimeout 的主要问题在于,它并非那么精确.譬如通过 setTimeout() 设定一个任务在 10 毫秒后执行,但是在 9 毫秒之后,有一个任务占用了 5 毫秒的 CPU 时间片,再次轮到定时器执行时,时间就已经

js异步编程理解

1.概念 同步:一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的.同步的.异步:每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的.异步的. 案例分析:欣多多去火车票代售窗口买票,现在该欣多多购票了,欣多多后面是小王珂,他也等着买票. 同步解析:欣多多:买一张明天到大理的火车票售票MM:对不起,明天到大理的火车票已经没有了欣多

深入理解node.js异步编程

1. 概述目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平台,一开始就注定会引人瞩目. 当然能够吸引众人的目光,肯定不是三教九流之辈,必然拥有独特的优势和魅力,才能引起群猿追逐.其中当属异步IO和事件编程模型,本文据Node.js的异步IO和事件编程做深入分析. ##2. 什么是异步同步和异步是一个比较早的概念,大抵在操作系统发明时应该就出现了.举一个最简单的生活中的例子,比如发短信的情况会比较好说明他们的区别:同步:正在处于苦逼