文章目录:
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
虽然现在各个浏览器对ES6的支持度已经越来越高,但是始终不完整,同时也为了兼容不同版本的浏览器实现,于是在一些情况下我们可能需要借助于一些ES6的转码器将ES6代码转化成ES5标准的代码。
在边理解ES6,边写一些ES6相关的代码片段时,我喜欢用[ babeljs-online ]。用它我们可以不用在文本编辑器和命令行控制台之间来回切换,就可以写并且实时运行ES6的代码了。
它是长成这个样子的:
左边可以直接写ES6语法的代码,左下角提供实时的错误检测;
右边是实时编译成的ES5语法的代码,右下角可以输出console.log();
当然我们也可以安装babel模块:
1 npm install --global babel 2 babel-node
这样就像在REPL中一样,我们可以写然后执行ES6代码了:
不过babel-node现在还不支持多行输入。
Traceur 同样是一个在线编辑器,在线的将ES6的代码转为ES5的代码。
我比较喜欢babel的在线编辑器,有实时的语法错误提醒和运行结果的输出。一般情况下我选择它写一些代码片段。
通过一些简单的代码片段,我们来看一下Generator的含义和其中的一些要点。可以在[ babeljs-online ]中自行键入代码理解其含义。
这里有一个简单的例子:
1 function* helloGenerator(){ 2 yield 'hello'; 3 yield 'generator'; 4 return 'ending'; 5 } 6 7 var hg = helloGenerator(); 8 9 console.log(hg.next()); 10 console.log(hg.next()); 11 console.log(hg.next()); 12 console.log(hg.next());View Code
查看运行结果:[ helloGenerator代码片段 ]
2.1yield语句的执行会暂停当前函数的执行并保存当前的堆栈,返回当前yield语句的值。
2.2Generator函数与普通的函数不同,它只定义了遍历器,不会执行,每次调用这个遍历器的next方法,就从函数体的头部或者上一次停下来的地方开始执行,直到下一个yield语句为止。
yield语句就是暂停的标志,next方法遇到yield,就会暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value属性值。当下一次调用next方法时,再继续向下执行,知道遇到下一个yield语句。如果没有再遇到新的yield语句,就一直运行到函数结束,将return语句后面的表达式的值,作为value属性的值,如果该函数没有return语句,则value属性的值为undefined。
2.3如果Generator函数不使用yield语句,这是它就变成了一个单纯地暂缓执行函数:
1 function* f(){ 2 console.log('开始执行了'); 3 } 4 5 var generator = f(); 6 7 setTimeout(function(){ 8 generator.next(); 9 },2000)View Code
查看运行结果:[ 暂缓执行的Generator函数 ]
2.4yield语句不能用在普通函数中,否则会报错:
1 var arr = [1,[[2,3],4],[5,6]]; 2 3 var flat = function* (a){ 4 a.forEach(function(item){ 5 if(typeof item!=='number'){ 6 yield* flat(item); 7 }else{ 8 yield item; 9 } 10 }) 11 }; 12 13 for(var f of flat(arr)){ 14 console.log(f); 15 }View Code
查看运行结果:[ 在普通函数中使用yield语句报错的例子 ]
可以看到编辑器报错了。
2.5yield语句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当做上一个yield语句的返回值。但是由于next方法的参数表示上一个yield语句的返回值,所以第一次使用next方法时,不能带有参数,V8会直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。
2.6for...of循环可以自动遍历Generator函数,并且此时不需要再调用next方法:
1 function *foo(){ 2 yield 1; 3 yield 2; 4 yield 3; 5 yield 4; 6 return 5; 7 } 8 for(let v of foo()){ 9 console.log(v); 10 }View Code
上面的代码会输出1,2,3,4并不会输出5,这是因为一旦next方法返回的对象的done属性为true,forof循环就会停止。
2.7如果yield语句后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器,这又叫yield*语句。
可以看下这个例子:
1 let delegatedIterator = (function* (){ 2 yield 'Hello!'; 3 yield 'Bye!'; 4 })(); 5 6 let delegatingIterator = (function* (){ 7 yield 'Greeting'; 8 yield* delegatedIterator; 9 yield 'Ok,bye!'; 10 })(); 11 12 for(let value of delegatingIterator){ 13 console.log(value); 14 }View Code
查看运行结果:[ yield*语句 ]
其实yield*语句就等同于在Generator函数内部部署一个for...of循环。
更多关于Generator函数的细节,严重推荐阅读阮一峰老师的ES6入门:[ ES6 入门 - 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 });
最先可以想到的方案:
1.读取当前文件夹下的所有文件;
2.获取到每个文件的stat,在确认IO操作都完成了的情况下比较文件大小;
3.过滤掉目录等 只比较文件;
4.通过callback返回最大的文件的filename。
实现起来应该是这个样子的:
1 var fs = require('fs'); 2 var path = require('path'); 3 4 module.exports = function(dir,cb){ 5 //获取当前目录下的所有文件 6 fs.readdir(dir,function(err,files){ 7 if(err) return cb(err); 8 //用于标识所有的文件IO操作已完成 9 var counter = files.length; 10 //为了避免出错时的callback被调用多次 11 var errored = false; 12 var stats = []; 13 files.forEach(function(file,index){ 14 15 fs.stat(path.join(dir,file),function(err,stat){ 16 //console.log(stat); 17 //stat(中文:统计)中包含了文件的一些相关信息 18 if(errored) return; 19 if(err){ 20 errored = true; 21 return cb(err); 22 } 23 stats[index] = stat; 24 25 if(--counter == 0){ 26 var largest = stats 27 .filter(function(stat){return stat.isFile()}) 28 .reduce(function(prev,next){ 29 if(prev.size > next.size) return prev; 30 return next; 31 }) 32 cb(null,files[stats.indexOf(largest)]); 33 } 34 }); 35 36 }); 37 }); 38 }View Code
再正常和正确不过的方案了,但是其中会有一些问题。可以看到我们通过counter变量用于确保所有的IO操作都已完成时才开始文件的比较;通过errored变量来确保出错时只调用一次错误回掉。这样可以看到上面的这段程序在管理并行的操作的时候需要额外的小心。
为了让代码的重用和测试变得简单些,我们可以稍稍改进下代码,提取出可充用的函数和方法:
1 var fs = require('fs'); 2 var path = require('path'); 3 4 module.exports = function(dir,cb){ 5 fs.readdir(dir,function(err,files){ 6 if(err) return cb(err); 7 var paths = files.map(function(file){ 8 return path.join(dir,file); 9 }); 10 11 getStats(paths,function(err,stats){ 12 if(err) return cb(err); 13 var largestFile = getLargestFile(files,stats); 14 cb(null,largestFile); 15 }); 16 17 }); 18 } 19 20 //获取文件的相关信息 21 function getStats(paths,cb){ 22 var counter = paths.length; 23 var errored = false; 24 var stats = []; 25 paths.forEach(function(path,index){ 26 fs.stat(path,function(err,stat){ 27 //查看对应文件的相关信息 28 //console.log(path+':'+ JSON.stringify(stat)); 29 if(err) return ; 30 if(err){ 31 errored = true; 32 return cb(err); 33 } 34 stats[index] = stat; 35 if(--counter == 0){ 36 cb(null,stats); 37 } 38 }); 39 }); 40 } 41 42 //获取最大的文件 43 function getLargestFile(files,stats){ 44 var largest = stats 45 .filter(function(stat){return stat.isFile()}) 46 .reduce(function(prev,next){ 47 if(prev.size > next.size) return prev; 48 return next; 49 }) 50 return files[stats.indexOf(largest)]; 51 }View Code
然而这还没有做出一些实质性的改进,可以看到,我们还是通过两个变量手动的管理了程序的流程。
我们使用很流行的 async 模块来改写下我们的程序:
1 var fs = require('fs'); 2 var async = require('async'); 3 var path = require('path'); 4 5 module.exports = function(dir,cb){ 6 //async.waterfall 提供一系列的执行,data可以从一个函数传递到下一个函数中,通过名为next的callback 7 async.waterfall([ 8 function(next){ 9 fs.readdir(dir,next); 10 }, 11 function(files,next){ 12 var paths = files.map(function(file){ return path.join(dir,file) }); 13 //async.map 可以并行的执行fs.stat然后返回一个结果数组 14 async.map(paths,fs.stat,function(err,stats){ 15 next(err,files,stats); 16 }) 17 }, 18 function(files,stats,next){ 19 console.log(stats); 20 var largest = stats 21 .filter(function(stat){return stat.isFile()}) 22 .reduce(function(prev,next){ 23 if(prev.size > next.size) return prev; 24 return next; 25 }) 26 next(null,files[stats.indexOf(largest)]); 27 } 28 ],cb) 29 }View Code
这里比较关键的两个方法:
1.async.waterfall,它提供一系列的执行流程,通过一系列的回调data可以从一个函数传递到下一个函数;
2.async.map,可以并行的执行fs.stat然后返回一个结果数组;
可以看到async只提供了一个callback,我们不用在担心流程的控制和回调函数被调用的次数了。
promises是为了结局多重嵌套的回调而提出的一种解决办法。它不是新的语法功能,只是一种新的写法。它允许将回调函数的横向加载改成纵向加载。
我们来用promises写法改写下代码:
1 var fs = require('fs'); 2 var path = require('path'); 3 var Q = require('q'); 4 var fs_readdir = Q.denodeify(fs.readdir); 5 var fs_stat = Q.denodeify(fs.stat); 6 7 module.exports = function(dir){ 8 return fs_readdir(dir) 9 .then(function(files){ 10 var promises = files.map(function(file){ 11 return fs_stat(path.join(dir,file)); 12 }) 13 return Q.all(promises).then(function(stats){ 14 return [files,stats]; 15 }) 16 }) 17 .then(function(data){ 18 var files = data[0]; 19 var stats = data[1]; 20 console.log(stats); 21 console.log(files); 22 var largest = stats 23 .filter(function(stat){return stat.isFile()}) 24 .reduce(function(prev,next){ 25 if(prev.size > next.size) return prev 26 return next 27 }) 28 return files[stats.indexOf(largest)]; 29 }) 30 }View Code
Q.all将会并行的获取到所有文件的stats并且返回一个数组。
其实代码一眼看上去是有些冗余的,原来的任务被promises包装了一下,看上去是好多的then,语义变得有些不清楚。
更多的关于promises和Q模块可以阅读:[ Promises in nodejs with Q ]
我们使用ES6的Generator这个异步编程的解决方案,来改进代码:
1 var fs = require('fs'); 2 var path = require('path'); 3 var co = require('co'); 4 var thunkify = require('thunkify'); 5 6 var readdir = thunkify(fs.readdir); 7 var stat = thunkify(fs.stat); 8 9 module.exports = co.wrap(function* (dir){ 10 var files = yield readdir(dir); 11 var stats = yield files.map(function(file){ 12 return stat(path.join(dir,file)); 13 }); 14 var largest = stats 15 .filter(function(stat){return stat.isFile()}) 16 .reduce(function(prev,next){ 17 if(prev.size > next.size) return prev 18 return next; 19 }) 20 return files[stats.indexOf(largest)]; 21 })View Code
这里使用了co模块和Thunk函数,后面会有详细的解释,这里先跳过。
我们可以看到几行代码精巧的解决了问题,并且语义清晰,同时回调的问题也不复存在。
我们看到了使用co封装了Generator的更加优雅和简单的异步编程方式。
可以在这里查看上述所有代码:[ 查找当前目录下的最大文件 ]
从上面的最后一个代码片段中我们可以看出,这里的Generator与原生的Generator 有一定的不同,因为koa中的Generator使用了 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的流转封装好了。
我们将参数放在一个临时函数中,再将这个临时函数传入函数体,当用到该参数时对临时函数求值即可。这个临时函数就叫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函数的含义和用法 ]
[ ES6 Generator ]
[ sync 模块 ]
[a sync函数的含义和用法 ]
[ Generator函数的含义和用法 ]
[ Thunk函数的含义和用法 ]
[ co函数的含义和用法 ]