当前位置: 首页 > 后端技术 > Node.js

一起来学习Promise

时间:2023-04-03 17:08:38 Node.js

吧。注意,本文主要关注ES6标准实现的Promise语法。示例代码也使用ES6语法。关于ES6的快速介绍,请参考ECMAScript6Literacy。一分钟快速启动,你怕被叫回地狱吗?试试承诺!.Promise的核心思想其实很简单,就是用Promise对象的方法注册对异步操作结果的处理,然后等到异步操作完成后,再访问这些处理操作。至于使用哪种处理操作,取决于Promise对象的状态。Promise对象有三种状态:Pending(初始状态)、Fulfilled(异步操作成功)、Rejected(异步操作失败)。三者之间的转换只有两种情况:Pending—>Fulfilled,Pending—>Rejected;详见下图:了解状态及其转换后,我们就可以使用Promise对象了:操作成功//异步操作失败时调用reject(error)});上面代码中传递给Promise构造函数的两个函数resolve、reject,分别用于触发Promise对象的Fullfilled和Rejected状态。Promise在Fullfilled状态时会调用then方法,在Rejected状态时会调用catch方法。这两个方法都会返回Promise对象,所以我们可以使用链式写法:promise.then((value)=>{...}).catch((error)=>{...});在上面的方法链中,then方法注册了Fullfilled状态的处理函数,catch方法注册了Rejected状态的处理函数。这种简单明了的写法,将异步操作的结果处理函数分离出来。如果这些处理本身就是异步操作,那么我们自然会从回调地狱中剥去层层异步回调,代码瞬间清爽。!深入Promise调用链,我们只是将一层处理操作分离到then方法中(其中catch方法只是then方法的一个语法糖,后面会解释);但在实际应用中,多个异步操作往往以序列化或并行的方式连续出现,比如下面的订房流程:数据校验、向API发送请求、向数据库中插入数据等都是异步操作。回调方法如下所示:validate(data,(err)=>{if(err)returnerrorHandler(err);request(apiUrl,(err,apiResponse)=>{if(err)returnerrorHandler(err);if(apiResponse.isSuccessful)insertToDB(data,(err)=>{if(err)returnerrorHandler(err);successHandler();});elseerrorHandler(newError('APIerror'));});});根据前面学习的Promise用法,我们已经可以使用validate的异步操作,写成Promise的形式:letpromiseValidate=newPromise((resolve,reject)=>{validate(data,(err)=>{if(err)returnreject(err);resolve();});});promiseValidate(data).then(()=>{request(apiUrl,(err,apiResponse)=>{如果(错误)返回错误处理程序(错误);if(apiResponse.isSuccessful)insertToDB(data,(err)=>{if(err)returnerrorHandler(err);successHandler();});elseerrorHandler(newError('API错误'));});}).catch((err)=>errorHandler(err));但要改就改到最后。上面的Promise和callback混合写法,除了还有callback嵌套判断处理多次错误的问题,也有点违背DRY,所以接下来我们深入研究Promise调用链的行为,重点关于then方法中注册的回调对调用链上数据传递和Promise对象状态变化的影响,以及如何统一处理调用链上的错误。Promise.resolve和Promise.reject先来看下一个“快速”生成Promise对象的方法:直接调用Promise.resolve(value)或Promise.reject(err)。这个方法和一个新的Promise对象的区别在于,Promise对象的状态在生成的时候就已经确定了,要么是Fullfilled(使用Promise.resolve()),要么是Rejected(使用Promise.reject())。新实例化等同于等待异步操作完成再改变。另外,如果传递给Promise.resolve方法的对象是带有then方法的对象(所谓的Thenable对象),比如jQuery的$.ajax(),那么返回的Promise对象,then的后续调用就是原始对象相同形式的then方法(见下面的代码)。简单地说,Promise.resolve将Thenable对象转换为ES6Promise对象。该特性常用于将Promise的不同实现转换为ES6实现。$.ajax('https://httpbin.org/ip').then((value)=>{/*输出223.65.191.59*/console.log(value.origin)});Promise.resolve($.ajax('https://httpbin.org/ip')).then((value)=>{/*输出223.65.191.59*/console.log(value.origin)});Promise.prototype.then的详解有了前面知识的铺垫,我们终于可以详细说说Promise对象的then方法了。参数上面说了,catch方法只是then方法的一个语法糖,因为then方法的参数其实是“两个”回调函数,分别用来处理调用它的Promise对象的Fullfilled和Rejected状态,而catch方法等同于then(undefined,Rejectedstatehandler)。关于这两个回调函数,首先注意是异步调用的:varv=1;/*输出结果:2*/Promise.resolve().then(()=>{console.log('result:'+v)});/*输出结果:2*/Promise.reject().then(undefined,()=>{console.log('result:'+v)});v++;以及两个回调函数通过调用then方法的Promise对象来指定参数:newPromise()生成的Promise对象会由内部resolve()的参数Promise.resolve()或Promise.reject()生成和reject()函数分别是Promise对象,分别使用Promise.resolve()、Promise.reject()参数和两个回调函数的返回值,会使用Promise.resolve(第一个回调返回值)或Promise.reject(第二个Callback返回值)用于“替换”then方法返回的Promise对象。结合上面提到的then回调函数的参数指定方法,callback的返回值会影响下一个then的回调函数:返回的是普通数据,会传递给调用的then方法nextlevel作为回调函数的参数,返回的是一个Promise对象或者Thenable对象,将用于“替换”then方法返回的Promise对象。then的回调函数如何调用和传递参数取决于一个返回返回值的newPromise对象的内部实现。状态取决于执行哪个回调函数。决定。注意这是一个新对象,不是简单修改调用then的Promise对象然后返回:varaPromise=newPromise((resolve)=>resolve(100));varthenPromise=aPromise.then((value)=>console.log(value));varcatchPromise=thenPromise.catch((error)=>console.error(error));/*true*/console.log(aPromise!==thenPromise);/*true*/console.log(thenPromise!==catchPromise);chaincall在了解了then方法的具体细节之后,我们就可以理解Promise调用链了:传递数据的方法:使用上面提到的then回调的参数传递形式——不管是在then方法的生成过程中直接传递Promise对象或者间接传入then回调的返回值——每一级异步操作的结果都可以传递给后续then中注册的处理函数进行处理。传递和改变Promise对象状态的方法:通过then回调的返回值,可以控制promise对象和then方法在一次操作后返回的状态。现在我们把所有的异步操作都改成Promise语法,然后在Promise调用链中使用传递数据和控制状态的方式来扩展本节开头提到的订房操作中的回调嵌套:letpromiseValidate=newPromise((resolve,reject)=>{validate(data,(err)=>{if(err)returnreject(err);resolve();});});letpromiseRequest=newPromise((resolve,reject)=>{request(data,(err,apiResponse)=>{if(err)returnreject(err);//Promise对象生成时直接传递异步操作的结果resolve(apiResponse);});});letpromiseInsertToDB=newPromise((resolve,reject)=>{insertToDB(data,(err)=>{if(err)returnreject(err);resolve();});});promiseValidate(data).then(()=>promiseRequest(apiUrl)).then((apiResponse)=>{//控制then回调的返回值改变then方法返回的新Promise对象的状态if(apiResponse.isSuccessful)returninsertToDB(数据);elseerrorHandler(newError('API错误'));}).then(()=>successHandler()).catch((err)=>返回errorHand勒(错误));上面的代码不仅扩展了嵌套代码,让我们摆脱了“回调地狱”;但是对于异步操作的错误也可以直接使用统一的Promise错误处理方式,避免写一堆重复的代码if为了进一步DRY,可以抽象出一个函数,封装一个典型的Node.js回调接口作为Promise接口:/*handle形式为receiver.fn(...args,(err,res)=>{})*/letpromiseify=(fn,receiver)=>{return(...args)=>{//Return重新打包的Promise接口returnnewPromise((resolve,reject)=>{fn.apply(receiver,[...args,(err,res)=>{//重新绑定这个returnerr?reject(err):resolve(资源);}]);});};};/*用例*/letpromiseValidate=promisify(validate,global);letpromiseRequest=promisify(request,global);letpromiseInsertToDB=promisify(insertToDB,global);注意,由于resolve和reject方法只能接收一个参数,所以上面函数处理的回调只能有err和一个data参数。Promise调用链上的错误处理Promise调用链上的错误处理思路是触发Promise对象的Rejected状态,利用状态的传递特性来捕获错误,然后在catch或catch中处理这些错误然后回调。下面来讨论一下:错误捕获首先,我们需要详细了解Promise对象的Rejected状态的生成和传递过程。Rejected状态的产生有两种情况:调用reject函数:Promise对象实例化的回调调用reject(),或者直接调用Promise.reject()通过throw抛出错误,只要Rejectedstate产生后,会在调用链中不断传递,直到遇到Rejected状态的处理回调(catch的回调或者then的二次回调)。结合前面提到的Promise调用链上的数据传递方式,错误可以被相应的回调“捕获”为调用链上的参数。这个过程可以看下图:这里需要注意的是,通过throw抛出错误时,如果错误是在setTimeout等回调中抛出的,不会导致Promise对象产生Rejected状态,即也意味着无法捕获Promise调用链。这个错误。例如,以下代码将没有输出:Promise.resolve().then(()=>setTimeout(100,()=>{thrownewError('hi')})).catch((err)=>console.log(err));原因是setTimeout的异步操作和Promise的异步操作不属于同一个任务队列,setTimeout回调中的错误会直接抛到全局成为UncaughtError。而不是作用于Promise对象及其调用链。这也意味着,如果要保证调用链上产生的错误能够被捕获,就必须始终使用调用reject函数的方法来产生并传递错误。错误处理错误处理可以在catch的回调中完成,也可以在then的二次回调中完成。虽然前面提到catch方法等同于then(undefined,Rejectedstatehandler),但是还是建议始终使用catch来处理错误,原因有二:代码的可读性对于then(Fullfilledstatehandler,Rejected状态处理Function)这种写法,如果Fullfilled状态的处理函数出现错误,错误只会继续往下传递,同级的Rejected状态处理函数无法捕捉到错误。优化房间预订示例的错误处理。了解了Promise调用链的Error处理之后,我们再来回顾一下开头提到的房间预订示例。之前,我们的代码只是统一处理了异步操作中可能出现的错误,而API错误等执行错误并没有使用Promise调用链中捕获和处理错误的方法。为了进一步DRY,我们可以通过调用Promise.reject强制返回的Promise对象变为Rejected,共享一个统一的Promise错误处理:(apiResponse)=>{if(apiResponse.isSuccessful)returninsertToDB(data);//返回的Promise对象处于Rejected状态,共享统一的Promise错误处理elsereturnPromise.reject(newError('APIerror'));}Promise.all和Promise.race上面研究的多个异步操作往往存在上下文依赖关系,或者它们是“串行”执行的,只有前一个完成后才能执行下一个。但是有时候我们处理的异步操作可能没有依赖关系,比如处理多张图片。这时候,如果我们使用上面调用链的写法,只能等一张图片处理完,对应的Promise对象的状态发生变化。处理下一个似乎效率很低。因此,我们需要一个可以同时处理调用链中多个Promise对象的方法,Promise.all和Promise.race就应运而生了。这两个方法的相同点在于,它们都会接受一个Promise对象数组作为参数,将它们包装并返回到一个新的Promise实例中。它们的区别在于返回的Promise实例的状态如何变化:Promise.all:所有传入的Promise对象状态变为Fullfilled,最终状态变为Fullfilled;此时Promise.resolve(每个Promise对象解析参数),生成一个新状态的Promise对象并返回每个Promise对象。如果其中一个Promise对象被拒绝,则最终状态变为Rejected;此时Promise.reject(第一个被拒绝实例的reject参数),生成新状态的Promise对象返回Promise.race:只要传入的Promise对象之一先改变状态(Fullfilled或Rejected),则返回的Promise对象的状态将更改为相应的状态。通过这两个方法,我们可以在Promise调用链上“并行”等待一些异步操作。让我们以上面提到的客房示例为例。如果我们在订房时需要请求多个API,那么调用链可以这样写:promiseValidate(data)/*请求多个API*/.then(()=>Promise.all([promiseRequest(apiUrl1),promiseRequest(apiUrl2)])).then((apiResponse)=>{/*传给下一个then回调是解析参数数组*/if(apiResponse[0].isSuccessful&&apiResponse[1].isSuccessful)returninsertToDB(data);elsereturnPromise.reject(newError('APIerror'));}).then(()=>successHandler()).catch((err)=>returnerrorHandler(err));Promise的应用Promise是一种异步调用的写法,自然是用来写清晰的异步代码,让我们摆脱回调写法带来的各种弊端。本文所用的预订房间的例子就是一个证明。但是考虑到实际的应用场景,还是有一些需要注意的地方:前端异步处理前端浏览器兼容性是阻碍新技术应用的一大问题。虽然现在的浏览器对ES6的支持越来越完善,但是除非不考虑IE(兼容表),直接在前端代码中使用原生的Promise实现不太现实。对于这种情况,我们可以使用一些Polyfill或者扩展类库来允许我们编写Promise代码。Node的异步处理:Node.js环境中的Promise从0.00版本开始支持ES6,所以我们在写服务端代码或者写一些运行在Node上的模块时可以直接使用Promise语法。不过需要注意的是,Node上大部分模块开放的API默认还是使用回调的方式。这是为了方便用户在不了解Promise语法的情况下快速上手。模块的内部实现可以随意使用。另外值得注意的是,最近Node实现了一种更加优雅的异步写法——async函数,但是新的写法是基于Promise的,所以虽然async函数的出现让Promise觉得实现起来还不够高和低,但是了解Promise的用法还是很有必要的,希望这篇文章可以帮助到你:D.参考JavaScriptPromise小书PromiseChainCallandAbort如何将Callback接口包装成Promise接口