简介在ES6中,除了上一篇文章中提到的新语法特性和一些新API外,还有两个非常重要的新特性Promise和Generator。今天我们就来详细解说这两个新特性。Promise什么是PromisePromise是一种异步编程的解决方案,比传统的“回调函数和事件”的解决方案更加合理和强大。所谓Promise,简单来说就是一个容器,里面装的是将来会结束的事件(通常是异步操作)的结果。从语法上讲,Promise是一个对象,可以从中获取异步操作的消息。Promise的特点Promise有两个特点:对象的状态不受外界影响。Promise对象表示一个异步操作,有三种状态:Pending(进行中)、Resolved(完成,也称为Fulfilled)和Rejected(失败)。只有异步操作的结果才能确定当前处于哪个状态,其他任何操作都不能改变这个状态。状态一旦改变,就不会再改变,随时都可以得到这个结果。改变Promise对象的状态只有两种可能性:从Pending到Resolved和从Pending到Rejected。这与事件(Event)完全不同。事件的特点是错过了再听,就得不到结果。Promise的优点Promise在同步操作的过程中表达了异步操作,避免了层层嵌套的回调函数。Promise对象提供统一的接口,可以更轻松地控制异步操作??。Promise的缺点是Promise不能被取消。一旦创建,将立即执行,不能中途取消。如果没有设置回调函数,Promise内部抛出的错误将不会反映到外部。当处于Pending状态时,无法得知当前正在进行到哪个阶段(刚刚开始还是即将完成)。Promise的用法Promise对象是用于生成Promise实例的构造函数:varpromise=newPromise(function(resolve,reject){//...一些代码if(/*异步操作成功*/){resolve(value);}else{拒绝(错误);}});promise可以连接thenoperation,thenoperation可以连接两个函数参数,第一个函数参数是构建Promise时resolve的值,第二个函数参数是拒绝构建Promise的error。promise.then(function(value){//成功},function(error){//失败});我们看一个具体的例子:functiontimeout(ms){returnnewPromise(((resolve,reject)=>{setTimeout(resolve,ms,'done');}))}timeout(100).then(value=>控制台日志(值));Promise中调用了一个setTimeout方法,会定时触发resolve方法,并传入参数done。最后,程序输出完成。PromiseExecutionOrder一旦一个Promise被创建,它就会被立即执行。但是Promise.then中的方法会在一个调用周期后再次被调用。让我们看下面的例子:日志('Step3');});console.log('Step2');输出:Step1Step2Step3Promise.prototype.then()Then方法返回一个新的Promise实例(注意,不是原来的Promise实例)。因此,可以使用链式写法,即在then方法之后调用另一个then方法。getJSON("/users.json").then(function(json){returnjson.name;}).then(function(name){console.log(名称);});上面的代码使用then方法依次指定了两个回调函数。第一个回调函数完成后,将返回结果作为参数,传入第二个回调函数Promise.prototype.catch()。Promise.prototype.catch方法是.then(null)的别名,rejection),用于指定发生错误时的回调函数。getJSON("/users.json").then(function(json){returnjson.name;}).catch(function(error){console.log(error);});Promise错误具有“冒泡”属性,将向后传递,直到被捕获。即错误总会被下一个catch语句getJSON("/users.json").then(function(json){returnjson.name;}).then(function(name){console.log(name);}).catch(function(error){//处理之前所有的错误console.log(error);});Promise.all()Promise.all方法用于将多个Promise实例包装成一个新的Promise实例varp=Promise.all([p1,p2,p3]);只有当p1、p2和p3的状态成立时,p的状态才会成立。此时,p1、p2、p3的返回值组成一个数组,传递给p的回调函数。只要p1、p2、p3中的一个被拒绝,p的状态就变成被拒绝,第一个被拒绝的实例的返回值就会传递给p的回调函数。Promise.race()Promise.race方法还将多个Promise实例包装成一个新的Promise实例varp=Promise.race([p1,p2,p3]);只要有p1,p2,p3中的一个实例先改变状态,p的状态随之改变。第一个改变的Promise实例的返回值被传递给p的回调函数。Promise.resolve()Promise.resolve()将现有对象转换为Promise对象。Promise.resolve('js');//等价于newPromise(resolve=>resolve('js'));那么什么样的对象可以转换成Promise对象呢?参数是Promise实例参数是thenable对象参数不是有then方法的对象,或者根本不是对象Promise.reject()Promise.reject(reason)方法也返回一个新的Promise实例,即实例的状态为rejectedvarp=Promise.reject('error');//等同于varp=newPromise((resolve,reject)=>reject('error'));Promise.reject()方法的参数,将原封不动的作为拒绝原因,成为后续方法的参数。这与Promise.resolve方法不一致。Promise对象done()的回调链,不管是以then方法结束还是以catch方法结束,如果最后一个方法抛出错误,则可能不会被捕获(因为Promise内部的错误不会冒泡到)全球的)。因此,我们可以提供一个始终位于回调链末尾的done方法,并保证抛出任何可能出现的错误asyncFunc().then(f1).catch(f2).then(f3).done();finally()finally方法用于指定无论Promise对象的最终状态如何都将执行的操作。它和done方法最大的区别是它接受一个普通的回调函数作为参数,无论如何都要执行。server.listen(1000).then(function(){//dosomething}.finally(server.stop);Generator什么是GeneratorGenerator函数是ES6提供的一种异步编程解决方案从语法上,首先可以理解Generator函数是一个状态机,封装了多个内部状态。执行Generator函数会返回一个遍历器对象。形式上Generator函数是一个普通的函数,但是它有两个特点,第一,函数关键字和函数名之间有一个星号;第二,函数体内部使用yield语句来定义不同的内部状态.例如一个例子:function*helloWorldGenerator(){yield'hello';yield'world';return'ending';}vargen=helloWorldGenerator();输出:console.log(gen.next());console.log(gen.next());console.log(gen.next());{value:'hello',done:false}{value:'world',done:false}{value:'ending',done:true}yield遍历器对象next方法的运行逻辑如下:(1)当遇到yield语句时,暂停后续操作,并将紧跟在yield后面的表达式的值作为返回对象的value属性值。(2)下次调用next方法时,继续执行,直到遇到下一个yield语句。(3)如果没有遇到新的yield语句,则一直运行到函数结束,直到return语句。并将return语句后的表达式的值作为返回对象的value属性值。(4)如果函数没有return语句,则返回对象的value属性值是undefined。注意yield语句本身是没有返回值的,或者总是返回undefined。next方法可以带一个参数,该参数将用作前一个yield语句的返回值。function*f(){for(leti=0;true;i++){letreset=yieldi;如果(重置){i=-1;}}}letg=f();console.log(g.next());console.log(g.next());console.log(g.next(true));输出结果:{value:0,done:false}{value:1,done:false}{value:0,done:false}可以看到最后一步,我们用next传入的true来替换i的值,最后引出i=-1+1=0。让我们看另一个例子:function*f2(x){vary=2*(yield(x+1));varz=yield(y/3);返回(x+y+z);}varr1=f2(5);console.log(r1.next());console.log(r1.next());console.log(r1.next());varr2=f2(5);console.log(r2.next());console.log(r2.next(12));console.log(r2.next(13));输出结果:{value:6,done:false}{value:NaN,done:false}{value:NaN,done:true}{value:6,done:false}{value:8,done:false}{value:42,done:true}如果next不传值,yield本身没有返回值,所以我们将得到NaN。但是如果next传入一个具体的值,这个值就会代替yield,成为真正的返回值。yield*如果在Generator函数内部,调用另一个Generator函数,默认没有效果function*a1(){yield'a';yield'b';}function*b1(){yield'x';a1();yield'y';}for(letvofb1()){console.log(v);}output:xy可以看到在b1中调用a1没有效果。修改上面的例子:function*a1(){yield'a';yield'b';}function*b1(){yield'x';产量*a1();yield'y';}for(letvofb1()){console.log(v);}输出结果:xaby的异步操作的同步表示暂停Generator函数执行的效果,也就是说异步操作可以写在yield语句中,等到下一个方法被调用之后再执行。这样其实就相当于不用写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到next方法调用时才会执行。因此,Generator函数一个重要的实际意义就是处理异步操作和重写回调函数。让我们看看如何通过Generator获取Ajax结果。function*ajaxCall(){letresult=yieldrequest("http://www.flydean.com");让resp=JSON.parse(result);console.log(resp.value);}functionrequest(url){makeAjaxCall(url,function(response){it.next(response);});}varit=ajaxCall();it.next();我们使用yield来获取异步执行的结果。但是我们如何将这个yield传递给result变量呢?请记住,yield本身没有返回值。我们需要调用generator的next方法传入异步执行的结果。这就是我们在请求方法中所做的。Generator的异步应用什么是异步应用?所谓“异步”简单的说就是一个任务不是连续完成的。可以理解为任务被人为的分成了两部分。首先执行第一部分,然后执行其他任务。准备就绪后,返回并执行第一部分。两段。例如,如果有一个读取文件进行处理的任务,则该任务的第一段是向操作系统发送读取文件的请求。然后程序执行其他任务,等待操作系统返回文件,并继续执行任务的第二部分(处理文件)。这种不连续的执行称为异步。相应地,连续执行称为同步。由于是连续执行,不能插入其他任务,所以程序只能等待操作系统从硬盘读取文件。在ES6诞生之前,异步编程大致有四种方法。回调函数事件监听器发布/订阅Promise对象回调函数fs.readFile(fileA,'utf-8',function(error,data){fs.readFile(fileB,'utf-8',function(error,data){}})如果顺序读取两个以上的文件,就会出现多重嵌套,代码不是纵向开发的,而是横向开发的,很快就会变得一团糟,无法管理。因为多个异步操作形成了强耦合,只要由于一个操作需要修改,它的上层回调函数和下层回调函数可能都要相应修改,这种情况称为“回调函数地狱”,PromisePromise对象就是为了解决这个问题而提出的,并不是一个新的语法特性,但是一种新的写法,允许将回调函数的嵌套改为链式调用。letreadFile=require('fs-readfile-promise');readFile(fileA).then(function(){returnreadFile(fileB);}).then(function(data){console.log(data);})Thunk函数和异步函数自动执行有两种方式,一种是callbyvalue,一种是callby姓名。“Callbyvalue”(按值调用),即在进入函数体之前,计算出x+5(等于6)的值,然后用这个值传入函数f。C语言使用了这种策略。“按名称调用”,即直接将表达式x+5传入函数体,只在使用时才求值。编译器的“按名称调用”的实现往往是把参数放在一个临时函数中,然后把这个临时函数传入函数体。此临时函数称为Thunk函数。例如:functionf(m){returnm*2;}f(x+5);上面的代码等于:varthunk=function(){returnx+5;}functionf(thunk){returnthunk()*2;}在JavaScript语言中,Thunk函数替代的不是表达式,而是多参数函数,将其替换为仅接受回调函数作为参数的单参数函数。怎么解释呢?例如在nodejs中:fs.readFile(filename,[encoding],[callback(err,data)])readFile接收3个参数,其中encoding是可选的。我们以两个参数为例。一般来说,我们这样调用:fs.readFile(fileA,callback);那么有没有办法改写成单参数函数的级联调用呢?varThunk=function(fn){returnfunction(...args){returnfuncton(callback){returnfn.call(this,...args,callback);}}}varreadFileThunk=Thunk(fs.readFile);readFileThunk(fileA)(回调);可以看到上面的Thunk把两个参数的函数重写成了一个单参数函数的级联方法。换句话说,Thunk是一个接收回调并执行方法的函数。这样重写有什么用?Thunk函数现在可用于生成器函数的自动流管理。之前讲Generator的时候,如果Generator中有多个yield异步方法,那么我们需要在next方法中传入这些异步方法的执行结果。当然也可以手动传入异步执行结果。但是有没有办法自动完成呢?letfs=require('fs');letthunkify=require('thunkify');letreadFileThunk=thunkify(fs.readFile);letgen=function*(){letr1=yieldreadFileThunk('/tmp/file1');console.log(r1.toString());让r2=yieldreadFileThunk('/tmp/file2');console.log(r2.toString());}letg=gen();functionrun(fn){letgen=fn();functionnext(err,data){letresult=gen.next(data);如果(结果。完成)返回;结果。值(下一个);}next();}run(g);gen.next返回一个对象,对象的值就是Thunk函数。我们再次将下一个回调传递给Thunk函数,以触发下一个yield操作。有了这个执行器,执行Generator功能就方便多了。不管里面有多少异步操作,只要将Generator函数传入run函数即可。当然,前提是每一个异步操作都必须是一个Thunk函数,即yield命令后面必须有一个Thunk函数。综上所述,Promise和Generator是ES6中引入的非常重要的语法,下面的koa框架就是Generator的具体实现。我们会在后续文章中详细讲解koa的使用,敬请期待。本文作者:flydean程序那些事儿本文链接:http://www.flydean.com/es6-promise-generator/本文来源:flydean的博客欢迎关注我的公众号:《程序》那些事儿》最通俗的解读,最深的干货,最简洁的教程,还有很多你不知道的小技巧等你来发现!
