当前位置: 首页 > 科技观察

一篇文章中掌握Generator,使用Generator实现异步编程

时间:2023-03-12 10:12:42 科技观察

了解GeneratorGenerator是ES6提供的一种新的数据类型,可以调用Generator函数,但它与普通函数有些不同。它最大的特点是可以交出函数的执行权(即暂停执行):定义函数时在函数后面有一个*号。可以使用关键字yield进行多次返回。调用后并不会立即执行,而是返回一个指向内部状态的指针对象,是一个Iterator对象。在返回的遍历器对象上调用next方法会移动内部指针,使其指向下一个状态。将返回一个对象,表示当前阶段的信息。其中,value属性为yield语句后表达式的值,表示当前阶段的值;done属性表示Generator函数执行是否完成,即是否有下一阶段。return遍历器对象有一个throw方法可以抛出错误,抛出的错误可以被函数体中的try/catch代码块捕获。备注:yield只能在Generator中使用,在其他地方使用会报错。这里有一段代码作为例子:'}运行结果如下:执行Generator函数genDemo,返回的g是一个指针。此时并没有打印,函数体中的代码也没有执行。执行g.next()运行直到第一个yield并停止运行,返回{value:'hello',done:false}。value值表示yield后的运算结果,done表示整个Generator是否执行完毕。继续执行g.next(),停在第二个yield,后面还有代码,sodone为false。继续执行g.next(),执行返回,代码执行完毕,sodone为true。继续执行g.next(),因为已经执行过了,继续执行会返回{value:undefined,done:true}。注意:在上面的结果中,变量hello的打印值是未定义的。描述未分配。那么如何赋值呢?应该在执行next方法的时候通过传参来赋值。这样,数据就被输入到Generator函数体中了。下面是运行的例子:第一次执行g.next,第一次yield时停止运行。这时候hello变量还没有被赋值。第二次执行g.next时,传递了一个参数'wmm66',传递的参数会赋值给上次暂停时yield之前要赋值的变量,即上面代码中的hello变量。赋值完成后,将代码向下运行到下一个yield。错误处理代码也可以部署在Generator函数内部,以捕获在函数体外部抛出的错误。以下是示例代码和运行结果。function*genDemo(){try{console.log('hellobefore')consthello=yield'hello'console.log('helloafter',hello)yield'world'return'end'}catch(err){控制台.warn(err)}}运行g.throw抛出错误。Generator函数内部的try/catch可以捕获错误。发生错误后,Generator函数结束运行并返回{value:undefined,done:true}。Generator实现异步编程相对于异步,我们的思维更容易理解同步代码。异步编程的句法目标是让代码更像同步代码。比如下面的代码(这里假设逻辑要求读完test1.txt,再读test2.txt)。constfs=require('fs')fs.readFile('./test1.txt',function(err,data1){if(err)returnerrconsole.log(data1.toString())fs.readFile('./test2.txt',function(err,data2){if(err)returnerrconsole.log(data2.toString())})})这段代码是回调函数的方式。当有这样的顺序时,就会有多个函数嵌套,在阅读和理解上会造成障碍。这就是通常所说的“回调地狱”。让我们把它改成promise。constfs=require('fs')constpromiseReadFile=(file)=>{returnnewPromise((resolve,reject)=>{fs.readFile(file,function(err,data){if(err)returnreject(err)resolve(data)})})}promiseReadFile('./test1.txt').then(data1=>{console.log(data1.toString())}).then(()=>{returnpromiseReadFile('./test2.txt')}).then(data2=>{console.log(data2.toString())}).catch(err=>{console.error(err)})链式调用方式,逻辑清晰多了。我们打算使用Generator函数实现异步,编码如下。constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')console.log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}Generator函数和yield本身与异步无关。Yield用于改变函数的执行流程,控制函数的执行流程,而恰好这种控制流程机制可以简化回调函数和promises的调用。我们现在要做的就是写一个方法来自动控制Generator函数的流程,接收并返回程序的执行权。在此之前,让我们看一下thunk函数。thunk函数介绍最早的thunk函数起源于“按值调用”和“按名称调用”的争论。letx=1functionfn(m){returnm*2}fn(x+1)通过值命题调用:执行前计算。先计算x+1=2,然后将值2传给fn方法:m*2。callbyname的命题:只在执行时计算。将x+1直接传递给fn方法:(x+1)*2。Javascript是“按值调用”。在JavaScript语言中,Thunk函数替代的不是表达式,而是多参数函数,将其替换为单参数版本,并且只接受一个回调函数作为参数。//普通版本的readFileconstfn=fs.readFile(file,callback)//thunk版本的readFilefunctionThunk(file){returnfunction(callback){returnfn(file,callback)}}constthunkReadFile=Thunk(file)thunkReadFile(callback)这段代码中的thunkReadFile函数只接受回调函数作为参数。这个单参数版本称为Thunk函数。只要有回调,任何函数都可以转换为thunk。下面是一个简单的thunk转换器。constThunk=function(fn){returnfunction(){constargs=Array.prototype.slice.call(arguments)returnfunction(callback){args.push(callback)returnfn.apply(this,args)}}}使用上面的转换器,为fs.readFile生成一个Thunk函数。constreadFileThunk=Thunk(fs.readFile)readFileThunk(file)(callback)有个插件thunkify可以直接转换。constfs=require('fs')constthunkify=require('thunkify')constreadFile=thunkify(fs.readFile)readFile('./test1.txt')(function(err,str){//...})实现回调函数的异步Thunk函数现在可用于生成器函数的自动流管理。我们让Thunk函数在Generator函数的yield之后执行。我们已经转换了我们想要的代码。constfs=require('fs')constthunkify=require('thunkify')constreadFile=thunkify(fs.readFile)constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')控制台.log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}那么应该怎么执行呢?我们先来分析一下:第一个执行next方法会把对象中的值作为一个函数返回,这个函数可以传入一个回调,可以获取到./test1.txt文件中读取的数据。读出./test1.txt文件中的数据后,我们需要继续执行next。这时候我们需要传入从./test1.txt文件中读取的数据,这样就可以在Generator函数中将数据赋值给data1变量。然后同上...所以执行方式如下:constg=genReadFile()constr1=g.next()r1.value(function(err,data1){if(err)throwerrconstr2=g.next(data1)r2.value(function(err,data2){if(err)throwerrg.next(data2)})})我们写一个Generator自动执行器来执行Generator函数。functionrun(gen){constg=gen()functionnext(err,data){constresult=g.next(data)if(result.done)returnresult.value(next)}next()}run(genReadFile)实现Promise的异步思路和回调函数是一样的,具体代码如下:constfs=require('fs')constreadFile=(file)=>{returnnewPromise((resolve,reject)=>{fs.readFile(file,function(err,data){if(err)returnreject(err)resolve(data)})})}constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')控制台。log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}手动执行代码如下:constg=genReadFile()g.next().value.then(function(data1){g.next(data1).value.then(function(data2){g.next(data2)})})Generator自动执行代码如下:functionrun(gen){constg=gen()functionnext(data){const{value,done}=g.next(data)if(done)返回值value.then(function(data){next(data)})}next()}运行(genReadFile)co函数有成熟的函数库。co函数库接受Generator函数作为参数并返回一个Promise对象。Thunk函数示例:constfs=require('fs')constthunkify=require('thunkify')constco=require('co')constreadFile=thunkify(fs.readFile)constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')console.log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}co(genReadFile)Promise对象示例:constfs=require('fs')constco=require('co')constreadFile=(file)=>{returnnewPromise((resolve,reject)=>{fs.readFile(file,function(err,data){if(err)returnreject(err)resolve(data)})})}constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')控制台.log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}co(genReadFile)async/await相比ES7,引入了async函数。总之,async函数是Generator函数的语法糖。Generator函数封装的异步代码示例:constgenReadFile=function*(){constdata1=yieldreadFile('./test1.txt')console.log(data1.toString())constdata2=yieldreadFile('./test2.txt')console.log(data2.toString())}co(genReadFile)被重写为异步函数:constasyncReadFile=asyncfunction(){constdata1=awaitreadFile('./test1.txt')console.log(data1.toString())constdata2=awaitreadFile('./test2.txt')console.log(data2.toString())}asyncReadFile()对比上面两段代码,async函数就是星号Generator函数的*替换为async,放在function关键字前面,yield替换为await。async函数对generator函数的改进体现在以下几点:内置executor:generator函数的执行必须依赖executor(以上代码使用co模块执行),async函数自带与执行人。异步函数的执行与普通函数完全一样。更好的语义:async表示函数中有异步操作,await表示紧随其后的表达式需要等待结果。语义更清晰。适用性更广:根据co模块约定,yield命令后只能跟Thunk函数或Promise对象,而async函数的await命令后可以跟Promise对象和原始类型的值。返回值是一个Promise:async函数的返回值是一个Promise对象,比Generator函数的返回值是一个Iterator更重要。对象要方便得多。您可以使用then方法来指定下一步要做什么。