当前位置: 首页 > Web前端 > JavaScript

JS中关于Promise的一切

时间:2023-03-27 01:09:37 JavaScript

关于Promise的定义和基本使用,可以参考红皮书和MDN。在搞清楚Promise是什么之前,首先要弄清楚它为什么存在:Promise并不是一种新的语法,而是对回调函数这种异步编程方式的改进。Promise将嵌套调用改为链式调用,增加了可读性和可维护性;Promise和回调函数先总结一下:回调函数是JS中实现异步编程的方式之一,而Promise是解决回调地狱的方式之一。在JavaScript的世界中,所有代码都在单个线程上执行。由于这个“缺陷”,JavaScript的所有网络操作和浏览器事件都必须异步执行。以网络请求为例,如果需要在获取上一次请求的数据后发起下一次请求,可以这样写:ajax1(url1,()=>{doSomething1()ajax2(url2,()=>{doSomething2()ajax3(url3,()=>{doSomething3()})})})长此以往,再嵌套回调函数,就会形成常说的“回调地狱”。回调地狱的缺点很明显:代码耦合,可读性差,维护困难;如果你不能使用trycatch,你就无法排除故障。而Promise可以很好的解决“回调地狱”问题:ajax1(url1).then(res=>{doSomething1()returnajax2(url2)}).then(res=>{doSomething2()returnajax3(url3)}).then(res=>{doSomething3()}).catch(err=>{console.log(err)})可以看到Promise的优点是:将回调函数的嵌套调用改为链式调用,代码漂亮;如果在链式调用过程中发生错误,则会进入catch方法捕获错误;Promise还提供了其他强大的功能,如:race、all等;使用第三方提供的API时使用Promise重写回调函数,如果API是使用回调函数编写的,可以使用Promises重写。例如微信小程序发送请求的API:wx.request({url:'',//请求路径method:"",//请求方法data:{},//请求数据header:{},//Requestheadersuccess:(res)=>{//resresponsedata}})下面使用Promise重写,即在成功回调中resolve,在失败回调中reject:functionmyrequest(options){returnnewPromise((resolve,reject)=>{//创建Promisewx.request({url:options.url,method:options.method||"GET",data:options.data||{},header:options.header||{},success:res=>{resolve(res)//resolve成功回调},fail:err=>{reject(err)//reject失败回调}})})}使用这个自定义API:myrequest({url:'xxx',header:{'content-type':'json'}}).then(res=>{console.log(res)}).catch(err=>{console.log(err)})Promise的基本概念Promise是一个新的ES6对象,通过new实例化,实例化时传入一个执行器函数(executor)作为参数ting://executor函数有两个参数:resolve,reject,它们也是函数constpromise=newPromise(function(resolve,reject){//...一些代码if(/*asyncoperationsucceeded*/){解决(价值)}埃尔se{reject(error)}})Promise的特点是:对象的状态不受外界影响。Promise对象表示一个异步操作,具有三种状态:pending(进行中)、fulfilled(成功)和rejected(失败)。只有异步操作的结果才能确定当前状态,其他任何操作都不能改变这个状态;状态一旦改变,就不会再改变,随时可以得到结果。改变Promise对象的状态只有两种可能性:从pending到fulfilled和从pending到rejected。只要这两种情况发生,状态就会冻结,不会再发生变化,并永远保持这个结果。这时候,就叫做resolved(finalized);Promise的三种状态是Pending:等待;Fulfilled:完成,调用resolve;Rejected:reject,调用reject;从上图我们可以看出Promise的生命周期:Promise的初始状态是Pending;在创建Promise时,定义何时resolve和何时reject;then方法接收到resolve的结果,而catch接收到rejection的结果,此时Promise状态为Fulfilled或Rejected;then和catch方法会返回一个新的Promise,从而实现链式调用;Promise的链式调用是如何实现的?先看Promise链调用的一般写法:newPromise((resolve,reject)=>{setTimeout(()=>{resolve()})}).then(res=>{//自己处理...res=res+'111'//交给下一层处理returnres}).then(res=>{//自己处理...res=res+'222'//交给到下一层处理returnres})按照上图,then方法应该返回一个Promise对象继续调用then/catch方法,但是为什么这里直接返回res就可以了呢?因为返回值在then方法内部自动包装成一个Promise,上面的代码等价于:newPromise((resolve,reject)=>{setTimeout(()=>{resolve()})}).then(res=>{//自己处理...res=res+'111'//交给下一层处理returnPromise.resolve(res)}).then(res=>{//自己处理...res=res+'222'//交给下一层处理returnPromise.resolve(res)})Promise.resolve(res)是newPromise(resolve=>{resolve(res)})Promise的语法糖和microtask中的执行函数Promise是同步执行的,但是里面可能有异步操作。异步操作完成后会调用resolve方法,或者中间遇到错误会调用reject方法,两者都作为微任务进入事件循环。那么,Promise为什么要引入回调操作的微任务呢?如何处理异步回调,有两种方式:将回调函数放在宏任务队列的尾部。将回调函数放在当前宏任务的末尾(即作为微任务)。如果采用第一种方式,执行回调(resolve/reject)的时间应该在前面所有的宏任务完成之后。如果当前任务队列很长,回调会长时间不执行,导致应用卡顿。为了解决上述方案的问题,同时也考虑到延迟绑定的需求,Promise采用了第二种方式,引入microtasks,即将resolve/rejectcallbacks的执行放在当前macrotask的最后;Promise的执行顺序其实就是理解Promise的执行顺序就是理解Promise是如何进入事件循环的。前置知识:1:当前正在执行的每一段JS代码都放在JS的主线程中。synchronized的代码会按照代码顺序依次放入主栈,然后按照放入的先后顺序依次执行。2:异步代码会被放入microtask/macrotask队列,promise属于microtask。3:异步代码必须等到同步代码执行完毕后才能执行。也就是说,直到JSStack为空,microtask队列中的代码才会被放入主栈中,然后执行。4:新的Promise()和.then()方法是同步代码。5:.then(resolveCallback,rejectCallback)中resolveCallback和rejectCallback的执行是异步代码,会被放入微任务队列。6:调用resolve()起到两个作用:Promise从pending状态变为resolved状态;遍历注册在这个promise上的所有resolveCallback方法,依次加入microtask队列;7:.then()只是注册了回调方法,并不会将回调方法添加到微任务队列中(参考上面第6点)。让我们看几个例子:例子1newPromise((resolve,reject)=>{console.log(4)resolve(1)Promise.resolve().then(()=>{console.log(2)})}).then((t)=>{console.log(t)})console.log(3)//输出为:4321分析:newPromise的代码是同步执行的,所以它的参数,即执行器函数(resolve,reject)=>{}是同步执行的,所以立即执行打印4;resolve(1)会将外层pomise的状态从pending变为resolved,但是还没有执行到外层then,所以此时最外层的promise上没有注册回调方法,并且(t)=>{console.log(t)}无法添加到微任务队列;Promise.resolve()的结果已经resolve了,所以内部的then回调(print2)直接加入到微任务队列中;最后轮到外层then回调(打印1)加入微任务队列;此时主栈和微任务队列:JSStack:[print4,Print3]Microtask:[Print2,Print1]Example2newPromise((resolve,reject)=>{Promise.resolve().then(()=>{//cb1resolve(1)Promise.resolve().then(()=>{console.log(2)})//cb2})}).then((value)=>{console.log(value)})//cb3console.log(3)//输出:312分析:第2行then的回调(cb1)立即加入微任务队列;此时:JSStack:[print3]Microtask:[cb1]microtask在macrotask执行完后才开始执行(只有一个),先执行resolve(1),此时外层promise变为resolved,所以然后可以执行外层,然后将外层的回调(cb3)添加到微任务队列中;此时:JSStack:[]Microtask:[cb3]然后执行第4行,直接将cb2加入microtask队列;此时:JSStack:[cb3]Microtask:[cb2]Example3newPromise((resolve,reject)=>{Promise.resolve().then(()=>{//cb1resolve(1);Promise.resolve().then(()=>{console.log(2)})//cb2})Promise.resolve().then(()=>{console.log(4)})//cb3}).then((t)=>{console.log(t)})//cb4console.log(3);//Output:3412分析:第2行then的回调(cb1,cb3)并且6被立即添加到微任务队列中;此时:JSStack:[print3]Microtask:[cb1,cb3]宏任务执行完后会开始执行microtasks(只有一个),先执行resolve(1),然后外层promise变为resolved,所以外层然后层就可以执行了,然后将外层的回调(cb4)添加到微任务队列中;此时:JSStack:[cb3]Microtask:[cb4]先执行mainstack,打印4再执行第4行,直接将cb2加入microtask队列;此时:JSStack:[]Microtask:[cb2]Promiseandasync/awaitpassed根据上面的分析,Promise的链式调用是对“回调地狱”的一种优化,但是如果链式调用过长,不够漂亮。所以async/await就是进一步优化then链。如果是三步,每一步都需要上一步的结果:functiontakeLongTime(n){returnnewPromise(resolve=>{setTimeout(()=>resolve(n+200),n)})}functionstep1(n){console.log(`step1with${n}`)returntakeLongTime(n)}functionstep2(n){console.log(`step2with${n}`)returntakeLongTime(n)}functionstep3(n){console.log(`step3with${n}`)returntakeLongTime(n)}Promise链调用将像这样:functiondoIt(){console.time("doIt")consttime1=300step1(time1).then(time2=>step2(time2)).then(time3=>step3(time3)).then(result=>{console.log(`resultis${result}`)console.timeEnd("doIt")})}doIt()如果使用异步/等待实现:asyncfunctiondoIt(){console.time("doIt")consttime1=300consttime2=awaitstep1(time1)consttime3=awaitstep2(time2)constresult=awaitstep3(time3)console.log(`resultis${result}`)console.timeEnd("doIt")}doIt()结果和之前的Promise的实现是一样的,但是代码非常简洁,看起来和同步代码一样。我们来看看对async/await的理解:async用于声明一个函数是异步的,await用于等待一个异步方法完成;async是修饰符。async定义的函数默认会返回一个Promise对象resolve的值。如果函数中返回的是直接量,async会通过Promise.resolve()将直接量封装成一个Promise对象;awaitwaits是一个表达式,这个表达式的结果是一个Promise对象或者其他值(也就是说没有特殊的限制);如果await等待一个Promise对象,它会阻塞下面的代码,等待Promise对象resolve,然后获取resolve的值作为await表达式的运算结果。因此,所有的Promise链调用都可以转化为async/await形式。手写Promise如果你会手写Promise,那么你对其原理的理解自然会很深刻。如果你想手写一个Promise,你必须遵循Promise/A+规范。业界所有的Promise类库都遵循这个规范。结合Promise/A+规范,可以分析出Promise的基本特征:promise有三种状态:pending、fulfilled、rejected;《SpecificationPromise/A+2.1》新建promise时,需要传入一个executor()执行器,执行器会立即执行;executor接受两个参数,分别是resolve和reject;promise的默认状态是pending;promise有一个保存成功状态值的值,可以是undefined/thenable/promise;“SpecificationPromise/A+1.3”promise有理由保存失败状态的值;“SpecificationPromise/A+1.5”承诺只能从pending变为rejected,或从pending变为fulfilled。状态一旦确定,就不会再改变;promise必须有一个then方法,然后接收两个参数,分别是promise成功的回调onFulfilled,promise失败的回调onRejected;"SpecificationPromise/A+2.2"如果调用then时promise成功,则执行onFulfilled,参数为promise的值;如果调用then时承诺失败,则执行onRejected,参数是承诺的原因;如果then抛出异常,则异常会作为参数传递给下一个then失败回调onRejected;Promise的实现如下://Promise的三种状态constPENDING='PENDING';constFULFILLED='FULFILLED';constREJECTED='REJECTED';//CustomMyPromiseclassclassMyPromise{constructor(executor){this.status=PENDINGthis.value=undefinedthis.reason=undefined//存储成功的回调this.onResolvedCallbacks=[]//Storefailedcallbacksthis.onRejectedCallbacks=[]letresolve=(value)=>{if(this.status===PENDING){this.status=FULFILLEDthis.value=value//依次执行对应函数this.onResolvedCallbacks.forEach(fn=>fn())}}letreject=(reason)=>{if(this.status===PENDING){this.status=REJECTEDthis.reason=reason//将对应的函数转Executethis.onRejectedCallbacks.forEach(fn=>fn())}}try{executor(resolve,reject)}catch(err){reject(err)}}//then方法then(onFulfilled,onRejected){if(this.status===FULFILLED){onFulfilled(this.value)}if(this.status===REJECTED){onRejected(this.reason)}//如果promise的状态是pending,onFulfilled和onRejected函数需要被存储,等待状态确认,然后依次执行对应函数if(this.status===PENDING){this.onResolvedCallbacks.push(()=>{onFulfilled(this.value)})this.onRejectedCallbacks.push(()=>{onRejected(this.reason)})}}}使用自定义MyPromise:constpromise=newMyPromise((resolve,reject)=>{setTimeout(()=>{resolve('success');},1000)}).then((res)=>{console.log('success',res)},(err)=>{console.log('faild',err)})注意,以上只是Promise的一个简单版本,链式调用、价值穿透等特性还没有实现。参考链接Javascript异步编程的4种方法Promise为什么要引入微任务?Promise对象——阮一峰理解JavaScript的async/awaitJS——Promise执行顺序