ES6入门十一:Generator生成器、async+await、Promisify
- 生成器的基本使用
- 生成器 + Promise
- async+await
- Promise化之Promisify工具方法
一、生成器的基本使用
在介绍生成器的使用之前,可以简单理解生成器实质上生成的就是一个迭代器,所以如果理解了迭代器在学习生成器就会非常简单,我的上一篇博客就是迭代器的详细解析ES6入门:iterator迭代器。迭代器本身是一个非常简单的程序执行逻辑内容,但是它与异步回调Promise的综合应用带来了非常强大的代码组织和执行模式,所以要深入了解生成器的能力重点却是需要对Promise有非常深入的理解。ES6入门八:Promise异步编程与模拟实现源码
1.生成器的语法
1 function *fun(n){ 2 let a = yield n * 2; 3 let b = yield a + 5; 4 let c = yield b / 2; 5 return c; 6 } 7 let funGen = fun(12); 8 let resultObj1 = funGen.next();//{value: 24, done: false} 9 let resultObj2 = funGen.next(resultObj1.value);//{value: 29, done: false} 10 let resultObj3 = funGen.next(resultObj2.value);//{value: 14.5, done: false} 11 let resultObj4 = funGen.next(resultObj3.value);//{value: 14.5, done: true} 12 console.log(resultObj4.value); //14.5
语法解析:
1.1.生成器就是在普通函数名称与function关键字之间的任意位置标记一个“*”表示该函数是一个生成器。
1.2.生成器执行会返回一个Generator对象,也可以视该对象为一个Iterator,因为该对象同样可以被迭代。
1.3.生成器执行生成一个Generator对象的时候迭代器内部代码不会执行,而是需要通过对象调用next()方法才会执行内部代码。
1.4.Generator对象调用next()方法基于yield关键字迭代执行,next()方法第一次执行是从头开始执行到第一个yield的右侧代码,yield左侧代码会等到下一个next()调用才会执行。当所有yield关键被迭代完成以后,最后一个next()方法返回的对象中done属性值为true,表示该Generator被迭代到最末尾处。
1.5.被yield截断的表达式除了作为阻断代码执行的作用以外,yield关键字同时充当了表达式右侧代码的return功能,将右侧代码执行结果作为当前next()方法的返回值;yield关键还充当了左侧代码被next()调用时接收参数的功能。(yield关键之应该很容易理解,它的功能就是截断程序执行,并且通过返回值和接收参数的方式连接被截断的程序)
1.6.yield作用的return特性其返回值最后会被next()方法返回的对象中的value属性获取。
2.基于生成器实现数据集的迭代器:
1 let obj = { 2 0: "a", 3 1: "b", 4 2: "c", 5 length: 3, 6 [Symbol.iterator]:function *(){ 7 let currIndex = 0; 8 while(currIndex != this.length){ 9 yield this[currIndex]; 10 currIndex ++; 11 } 12 } 13 } 14 console.log([...obj]);//["a", "b", "c"]
通过上面的示例和解析,我们可以了解到生成器实质上就是打破传统函数的完整运行模式,在此之前编写JavaScript程序几乎普遍依赖一个函数执行机制:一个函数开始执行,就会运行到结束,期间不会有其他代码打断或插入到其中。这种完整执行看起来实非常理想的设计,但是在实际的生活中我们做某件事情的时候往往并不是完整的一个过程,或许生成器的设计更接近映射现实的抽象。
3.多个生成器交替执行:
1 function *foo(){ 2 var x = yield 2; 3 z++; 4 var y = yield (x * z); 5 console.log(x,y,z); 6 } 7 var z = 1; 8 var it1 = foo(); 9 var it2 = foo(); 10 11 var val1 = it1.next().value; 12 var val2 = it2.next().value; 13 14 val1 = it1.next(val2 * 10).value; 15 val2 = it2.next(val1 * 5).value; 16 17 it1.next(val2 / 2);//20 300 3 18 it2.next(val1 / 4);//200 10 3
上面的示例情景在普通的两个方法之间是不可能被实现的,要想实现每一步操作到得到最终的结果,按照之前完整执行的函数执行模式需要更多的函数来实现。而且这种设计方式更接近现实中的模式,这有利于程序设计更有利于代码阅读。
4.使用生成器作为一个生产者,替代迭代器生产数据,让生产变得更合理:
1 function *producer(){ 2 let num; 3 while(true){ 4 if(num === undefined){ 5 num = 1; 6 }else{ 7 num = (4 * num) + 3; 8 } 9 yield num; 10 } 11 } 12 for(var n of producer()){ 13 console.log(n); //1 7 31 127 511 2047 14 if(n > 800){ 15 break; 16 } 17 }
基于生成器取代迭代器生产数据相比,不需要手动写入next()、return()方法,也不要手动返回生成的数据,这减少了很大一部分代码。但是这里有一个缺点,通过前面的生成器执行分析我们知道最后一次返回的数据是由return()方法实现,但是它在基于for..of迭代到最后break触发return()方法时只能产生一个值,这会产生一种生成器还挂起的错觉,实际上这时候的生成器已经执行到了最后并且跳出了它内部的while循环器,所以以上代码还需改进:
1 function *producer(){ 2 try{ 3 let num; 4 while(true){ 5 if(num === undefined){ 6 num = 1; 7 }else{ 8 num = (4 * num) + 3; 9 } 10 yield num; 11 } 12 }finally{ 13 console.log("cleaning up!") 14 } 15 16 } 17 for(var n of producer()){ 18 console.log(n); //1 7 31 127 511 2047 cleaning up! 19 if(n > 800){ 20 break; 21 } 22 }
就单纯的生成器使用实非常的简单,而且由于JavaScript函数长期以来的执行模式导致实际开发并没有出现特别多的实际应用,但是Generator与Promise的结合给处理异步任务带来了很大的变化,使JavaScript异步编程变得更简单,代码更清晰更易读。
二、生成器 + Promise
1.回忆Promise异步回调,这里还是使用前面关于Promise相关内容博客的一个读取文件的示例:
//文件结构 --Generator.js --data ----number.txt //文件数据:./data/name.txt ----name.txt //文件数据:./data/score.txt ----score.txt //文件数据:99
基于Nodejs环境实现文件数据读取:
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 15 readFile('./data/number.txt').then((val) => { 16 return readFile(val); 17 }).then((val) => { 18 return readFile(val); 19 }).then((val) => { 20 console.log(val); 21 });
2.使用生成器取代Promise的链式调用:
上面基于Promise异步链式调用then实现的示例,这种链式调用是不是非常类似生成器的yield机制,获取上一个部分程序执行的数据作为下一部分程序执行的参数,在最后一步输出结果。也就是说Promise链式调用可以使用生成器的机制来实现,代码如下:
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 15 function *read(){ 16 let val1 = yield readFile('./data/number.txt'); 17 let val2 = yield readFile(val1); 18 let val3 = yield readFile(val2); 19 return val3; 20 }; 21 let oG = read(); 22 let {value, done} = oG.next(); 23 value.then((val) => { 24 let {value,done} = oG.next(val); 25 value.then((val) => { 26 let {value,done} = oG.next(val); 27 value.then((val) => { 28 console.log(val); 29 }); 30 }); 31 });
3.生成器递归委托:使用委托模式实现生成器自动迭代,使用递归消除next的重复调用
到了这一步你一定会恍然大悟,原来生成器的迭代过程不需要重复手动调用了,原来所有异步链式调用任务都可以委托给生成器来实现,在编写代码时只需要考虑每个执行环节的异步任务是什么就可以了,这让我们从复杂的异步任务中解脱了出来,相信这样的处理方式一定可以让你的异步编程效率大大提高。
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 15 function *read(){ 16 let val1 = yield readFile('./data/number.txt'); 17 let val2 = yield readFile(val1); 18 let val3 = yield readFile(val2); 19 return val3; 20 }; 21 22 function Co(oIt){ //生成器迭代委托 23 return new Promise((res,rej) =>{ 24 let next = (data) => { 25 let {value, done} = oIt.next(data); 26 if(done){ 27 res(value);//当迭代器到达最末尾时,将生成器的返回值传递给Promise的受理回调执行回调任务 28 }else{ 29 value.then((val) => { 30 next(val);//将上一个生成器返回值传递给下一个生成器的迭代方法next(这是个递归操作) 31 },rej);//在生成器迭代过程中如果发生异常会调用rej处理 32 } 33 } 34 next();//生成器第一次执行不需要参数 35 }); 36 } 37 38 Co(read()).then((val) => { 39 console.log(val); 40 },(val) => { 41 console.log(val); 42 });
三、async+await
在上一节中基于Generator+Promise给嵌套的异步任务提供了一个非常便捷的解决方案,这是异步任务非常典型的一种场景。有了生成器的函数执行新模式的出现,让标准进一步跟进规范这一场景的解决方案,在ES2017(ES8)标准引入async函数,使得异步操作变得更加方便,而实际上async函数就是Generator函数的语法糖。这里先不进行语法解析,直接看基于async函数如何改写第二节中的示例:
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 async function read(){ 15 let val1 = await readFile('./data/number.txt'); 16 let val2 = await readFile(val1); 17 let val3 = await readFile(val2); 18 return val3; 19 }; 20 read().then((val) => { 21 console.log(val); //99 22 })
相信代码已经说明一切了,从表面上看async将异步链式嵌套任务完全转化成了按照代码编写的先后顺序的同步执行任务,这里所指代的同步任务是指由async+await控制的内部异步任务,async函数本身是一个异步任务,它执行返回的是一个Promise对象用来处理异步链式任务的最后回调处理。所以在上面示例代码末尾添加一个输出任务会在async任务之前执行:
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 async function read(){ 15 let val1 = await readFile('./data/number.txt'); 16 let val2 = await readFile(val1); 17 let val3 = await readFile(val2); 18 return val3; 19 }; 20 read().then((val) => { 21 console.log(val); //99 22 }); 23 console.log("同步任务");
执行结果:
1 //同步任务 2 //99
既然是语法糖,就可以尝试使用babel来转码async函数,你会发现在转码后的代码中,其底层实现与第二节中Co实现非常类似,具体转码操作可以参考之前的相关博客或者其他教程,这里就不提供转码的相关内容了。
1.语法:
1 async function fun(){ 2 return "async"; 3 } 4 fun().then((val) => { 5 console.log(val); 6 }); 7 console.log("同步任务");
执行结果:
1 //同步任务 2 //async
从async本身来说就是将内部代码交给一个Promise作为excutor函数,然后将return返回值交给回调函数,Promise通过then注册的回调任务作为微任务(异步)处理。简单的说async函数返回一个Promise对象,并将返回值作为回调任务的参数,这里的底层实现可以直接参数Co理解,也可以称为内置执行器,转码工具中的函数名称是asyncReadFile()。接着来看await在async函数内的作用:
yield与await都是用来控制生成器的执行关键字,yield可以看作用来处理同步任务,await可以看作用来处理异步任务,两者都是用来控制程序的执行逻辑。
2.async函数的使用:
1 // 函数声明 2 async function foo(){} 3 // 函数表达式 4 let foo = async function(){} 5 // 对象方法 6 let obj = {async foo(){}}; 7 obj.foo().then(...); 8 // class的方法 9 class Storage{ 10 constructor(){ 11 this.cachePromise = caches.open('avatars'); 12 } 13 async getAvatar(name){ 14 const cache = await this.cachePromise; 15 return cache.match(`/avatars/${name}.jpg`); 16 } 17 } 18 const storage = new Storage(); 19 storage.getAvatar('jake').then(...); 20 //箭头函数 21 const foo = async () => {};
四、Promisify
在nodejs中有非常多的异步事件需要处理,为了便于处理回调任务通常会用到Promise,这本来是一件非常美好的事情,但是由于nodejs中有太多的异步任务了,你就会发现你的代码莫名其妙的出现了一种非常类是模式的代码,我们把这种情况叫做代码冗余。后来针对这种情况有大神提出了一套解决方案,使用一个工具方法来处理异步任务的Promise模式代码,这个工具方法也就被叫做异步任务Promise化。
下面我们来看我们使用Promise处理异步任务的代码(以前面读文件的素材为例):
1 let fs = require('fs'); 2 3 fs.readFile('./data/number.txt','utf-8',(err,data) => { 4 if(err){ 5 6 }else{ 7 console.log(data); 8 } 9 });
当看到这段代码时我们就能想到如果出现嵌套异步任务时,使用原生的异步方法必然会出现回调地狱的问题,然后我们针对这个问题给出下面的解决方案:
1 let fs = require('fs'); 2 3 function readFile(path){ 4 return new Promise((res,rej) => { 5 fs.readFile(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 15 readFile('data/number.txt').then((val)=>{ 16 console.log(val); 17 },(err)=>{ 18 console.log(err); 19 });
如果在这时候你没有意识到代码冗余的问题,你继续写下去就会发现一个非常恐怖的事情,比如除了上面直接读取文件内容的异步任务,还有读取目录内容的异步任务fs.readDir():
1 let fs = require('fs'); 2 3 function readdir(path){ 4 return new Promise((res,rej) => { 5 fs.readdir(path, 'utf-8', (err,data) => { 6 if(data){ 7 res(data); 8 }else{ 9 rej(err); 10 } 11 }); 12 }); 13 } 14 readdir('data').then((val) => { 15 console.log(val); 16 },(err) => { 17 console.log(err); 18 });
对比上面的代码就能很清楚的看到读取文件内容的异步任务和读取目录的异步任务除了调用的事件方法一样意外,其他代码完全一致,这种冗余对于一个优秀的程序员是绝对不允许的,所以就有了Promisify工具方法的出现:
1 let fs = require('fs'); 2 function promisify(func){ 3 return function(...arg){ 4 return new Promise((res,rej) =>{ 5 func(...arg,(err,data) =>{ 6 if(err){ 7 rej(err); 8 }else{ 9 res(data); 10 } 11 }); 12 }); 13 }; 14 } 15 let readFile = promisify(fs.readFile); 16 readFile('data/number.txt','utf-8').then((val)=>{ 17 console.log(val); 18 },(err)=>{ 19 console.log(err); 20 });
到了这里就完美解决了?notter!就单独拿nodejs中的文件系统模块(fs模块)来说,这个模块暴露出来的API就有几十个,虽然不全不是异步事件的API,但数量绝对不少,除了文件系统模块还有好多其他异步事件的API需要处理,还有就是我们也希望将一些同步的任务转换成异步任务呢?
这时有一个可以肯定的前提就是我们只需要将对应的模块传入一个工具方法执行处理,我们就能将这个模块的所有API进行Promise化。如果你对Promise有一定的了解的话,会知道Promise的回调也是一种异步任务,所以也就可以将同步的API使用同样Promise化也就可以将其转换成异步了,并且也实现了Promise化,下面这个工具方法就是在promisifyArr():
1 function promisifyAll(obj){ 2 for(let key in obj){ 3 let fn = obj[key]; 4 if(typeof fn === "function"){ 5 obj[key + 'Async'] = promisify(fn); 6 } 7 } 8 } 9 10 promisifyAll(fs);//对fs模块上所有方法进行Promise化 11 12 fs.readFileAsync('data/score.txt','utf-8').then((val) => { 13 console.log(val); 14 },(err) => { 15 console.log(err); 16 });
关于Promise工具方法在很多的工具库里面都有实现,比如bluebird就有实现promisify工具方法:
//初始化package.json npm init -y //下载bluebird工具库 npm install bluebird
然后再引入并使用bluebird库:
1 //bluebird 2 let bluebird = require('bluebird'); 3 4 let readFile = bluebird.promisify(fs.readFile); 5 readFile('data/number.txt','utf-8').then((val)=>{ 6 console.log(val); 7 },(err)=>{ 8 console.log(err); 9 });
控制台执行这个脚本:
node ....js//...表示省略的文件名称,更具你自己测试的js脚本名称调整