深入学习JavaScript中的promise
作者: 陈梵阿
为什么要用Promise?
我们知道JavaScript是单线程的,一次只能执行一个任务,会阻塞其他任务。因此,所有的网络任务、游览器事件等都是异步的,我们可以使用异步回调函数来进行异步操作。
有这么一个场景,我可以通过6个人能够认识到任何一个人。但是我们不知道当前的人联系到下一个人的时间是多久,假如这是一个异步的操作。
可以用如下代码表示:
function ConnectPeople(i) { console.log(`我联系到了第${i}个人`); return i + 1; } let i = 1; setTimeout(() => { const result1 = ConnectPeople(i); setTimeout(() => { const result2 = ConnectPeople(result1); setTimeout(() => { const result3 = ConnectPeople(result2); setTimeout(() => { const result4 = ConnectPeople(result3); setTimeout(() => { const result5 = ConnectPeople(result4); setTimeout(() => { const result6 = ConnectPeople(result5); setTimeout(() => { const result7 = ConnectPeople(result6); setTimeout(() => { const result8 = ConnectPeople(result7); setTimeout(() => { const result9 = ConnectPeople(result8); setTimeout(() => { const result10 = ConnectPeople(result9); }, 10000); }, 5000); }, 3000); }, 2000); }, 3000); }, 2000); }, 1000); }, 500); }, 2000); }, 1000);
如上所示,当我们联系到了第一个人后,再去联系第二个人,然后再去联系第三个人...直到我联系到了10个人。乍一看,代码好像还挺规整,但是如果100个人,1000个人呢?由于回调很多,函数作为参数层层嵌套,就陷入了回调地狱。这种情况下,就像是金字塔一样的代码非常不利于阅读。
但是还好,我们有解决办法。
使用Promise解决异步控制问题
什么是Promise?
Promise对象的主要⽤途是通过链式调⽤的结构
,将原本回调嵌套的异步处理流程,转化成“对象.then().then()...”的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调⽤的结构,这样就可以在阅读上编程上下左右结构的异步执⾏流程了。
因此,Promise的作⽤是解决“回调地狱”,他的解决⽅式是将回调嵌套拆成链式调⽤,这样便可以按照上下顺序来进⾏异步代码的流程控制。 如下代码所示,我们使用了Promise,代码也从原先的金字塔形式转变成了从上往下的执行流程。
function ConnectPeople(i) { console.log(`我联系到了第${i}个人`); return i + 1; } const p = new Promise((resolve) => { setTimeout(() => { resolve(ConnectPeople(1)); }, 1000); }); p.then((v1) => { return new Promise((resolve) => { setTimeout(() => { resolve(ConnectPeople(v1)); }, 1000); }); }).then((v2) => { return new Promise((resolve) => { setTimeout(() => { resolve(ConnectPeople(v2)); }, 1000); }); });
Promise的结构
根据上面的代码案例,我们发现Promise需要通过new关键字同时传入一个参数来创建,所以我们可以尝试打印一下window对象console.log(window)
(window 对象在浏览器中有两重身份,一个是ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口),可以发现存在一个Promise的构造函数。
Promise初始化的时候需要传入一个函数,如下所示:
const p = new Promise(fn) // fn是初始化的时候调用的函数,它是同步的回调函数
回调函数
什么是回调函数?
JavaScript中的回调函数结构,默认是同步的结构。由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。
为什么异步代码一定是回调函数结构?
我们知道JavaScript是单线程异步模型,严格按照同步在前异步在后的顺序执行。如果用默认的上下结构,我们拿不到异步回调中的结果。
如下所示,代码执行的时候会先执行同步代码,异步代码会挂起,继续执行同步代码,到1s的时候挂起的任务会进入队列,到2s的时候会继续执行同步代码打印1,然后从任务队列中取任务将num变成100,打印num。 所以实际执行效果是,过2秒后,先打印1再打印100。
var num = 1; setTimeout(()=>{ num = 100 console.log(num) },0) var d = new Date().getTime() var d1 = new Date().getTime() while ( d1 - d < 1000 ) { d1 = new Date().getTime() } console.log(num) // 1
刨析Promise
翻译一下promise,结果是承诺,保证
,红宝书中的解释是期约
。
它有三个状态:
- pending 待定,初始状态
- fulfilled 兑现,已完成,通常代表成功执行了某一任务。初始化函数中的resolve()执行时,状态就会变味fulfilled,而且.then函数注册的回调会开始执行,resolve中传递的参数会进入回调函数成为形参。
- rejected 拒绝,通常代表执行一次任务失败,调用reject()时,catch注册的函数就会触发,并且reject中传递的内容会变成回调函数的形参。
三种状态之间的关系:
当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏。
我们打印一下Promise对象,发现它的构造函数中定义了all
、allSettled
、any
、race
、reject
、resolve
方法(这些是实例方法),它的原型上存在catch
、finally
、then
方法(这些是原型方法)。
原型方法——catch\finally\then
首先看下面代码:
new Promise(function (resolve, reject) { resolve(); reject(); }) .then(function () { console.log("then执⾏"); }) .catch(function () { console.log("catch执⾏"); }) .finally(function () { console.log("finally执⾏"); });
执行后依次打印then执行->finally执行
,发现.catch的回调没有执行。
再看如下代码:
new Promise(function (resolve, reject) { reject(); resolve(); }) .then(function () { console.log("then执⾏"); }) .catch(function () { console.log("catch执⾏"); }) .finally(function () { console.log("finally执⾏"); });
这个串代码和之前的代码唯一的不同在于Promise中的回调先执行了resolve()还是先执行了reject(),打印结果是catch执行->finally执行
,发现.then的回调没有执行。
那如果Promise的回调不执行reject()和resolve()呢?
会发现什么输出都没有!
注意:Promise.prototype.catch()其实是一个语法糖,相当于是调用 Promise.prototype.then(null, onRejected)。.then中其实是可以传入2个回调函数,第一个回调函数是resolve()后执行,第二个回调函数是reject()后执行,2个是互斥的。
这是因为Promise的异步回调部分如何执⾏,取决于我们在初始化函数中的操作,并且初始化函数中⼀旦调⽤了resolve后⾯再执⾏reject也不会影响then执⾏,catch也不会执⾏,反之同理。
⽽在初始化回调函数中,如果不执⾏任何操作,那么promise的状态就仍然是pending,所有注册的回调函数都不会执⾏。
由此可见,执行完resolve()之后才能够执行.then的回调;执行reject()之后才能够执行.catch的回调;finally()的回调会在执行完.then或.catch之后执行。
这时候,我们就会想,是不是可以把resolve或者reject的调用设定在异步函数内去调用,这样是不是就能解决回调地狱的问题了?
所以我们就去尝试一下:
new Promise(function (resolve, reject) { setTimeout(() => { console.log(111); resolve(); }, 2000); }) .then(function () { return new Promise(function (resolve, reject) { setTimeout(() => { console.log(222); resolve(); }, 2000); }); }) .then(function () { return new Promise(function (resolve, reject) { setTimeout(() => { console.log(333); resolve(); }, 2000); }); }) .catch(function () { console.log("catch执⾏"); }) .finally(function () { console.log("finally执⾏"); });
上面代码每隔2s依次打印111->222->333 finally执行
。333执行后立马执行finally。
为什么要在.then的回调函数中return一个Promise呢?
因为下一个异步的执行,需要等待前一个异步执行完毕后才调用,我们需要用到resolve来控制.then执行的时机。
那如果我们不指明return返回值,它会返回什么呢?是如何实现链式调用呢?
看下面代码:
const p2 = new Promise((resolve, reject) => { resolve(); }); const p3 = p2.then(() => { console.log("resolved"); }); console.log(p3, 111);
p2.then的回调函数中没有return,但是我们知道一般来说函数返回值默认返回undefined,但是undefined中不会存在.then的方法。
因此我们就看一下p3里面到底是什么。有些人会想,.then是异步调用的,它是一个微任务,那访问p3是不是不太正确?
我们打印一下p3,就会看到如下信息:
第一行p3的状态还是pending
,当我们点开,发现已经变成了fulfilled
了,因为引用类型是按地址访问的,当我们点开的时候会发现指向这个地址里最后的数据是什么。普通对象同理。
如下所示,我们console的时候a对象的name还是a,但是我们点开后发现程序执行完后a对象的实际name变成了b。
const a = { name: "a" }; console.log(a); a.name = "b";
回归正传,我们发现p3里面有3个字段,[[Prototype]]
我们很熟悉,这个是一个指向当前对象原型的指针。在大多数游览器中是可以通过__proto__
访问到的。
我们尝试着去访问:
console.log(p3, 111); console.log(p3.__proto__); console.log(p3.__proto__ === Promise.prototype); // true
我们可以看到.then默认返回的有3个字段,然后通过原型链来实现链式调用:
- [[Prototype]]代表Promise的原型对象
- [[PromiseState]]代表Promise对象当前的状态
- [[PromiseResult]]代表Promise对象的值,分别对应resolve或reject传⼊的结果
本质就是在我们调⽤这些⽀持链式调⽤的函数的结尾时,他⼜返回了⼀个包含他⾃⼰的对象或者是⼀个新的⾃⼰,这些⽅式都可以实现链式调⽤。
中断链式调用的方式:
中断的⽅式可以使⽤抛出⼀个异常或返回⼀个rejected状态的Promise对象
链式调用的基本形式:
- 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve传⼊的值
- 后续每个函数都可以使⽤return返回⼀个结果,如果没有返回结果的话下⼀个then中回调函数的参数就是undefined
- 返回结果如果是普通变量,那么这个值就是下⼀个then中回调函数的参数
- 如果返回的是⼀个Promise对象,那么这个Promise对象resolve的结果会变成下⼀次then中回调的函数的参数
- 如果then中传⼊的不是函数或者未传值,Promise链条并不会中断then的链式调⽤,并且在这之前最后⼀次的返回结果,会直接进⼊离它最近的正确的then中的回调函数作为参数
前面几条我们都能懂,第5条什么意思的? 看下面代码:
const p2 = new Promise((resolve, reject) => { console.log(1); resolve(); }); p2.then(() => { console.log(2); return 123; }) .then() .then("456") .then((res) => { console.log(res); });
发现只打印了1 2 和 123,return的123进入了最后一个.then的回调函数中作为参数。
resolve和reject
至于resolve和reject,我们通过上面已经知道了resolve和reject能够更改Promise的状态,而Promise的状态是不可逆的,且是私有的。所以我们必须在Promise内部调用resolve或者reject。
当然,resolve和reject也能够传入参数,而传入的参数,会变为.then或.catch的回调函数中的参数。
那如果传入一个Promise作为参数呢???
resolve()
实际上,如果在resolve中传入一个promise,那它的行为就相当于是一个空包装。Promise.resolve()可以说相当于是一个幂等方法,会保留传入期约的状态。
let p = Promise.resolve(7); setTimeout(console.log, 0, p === Promise.resolve(p)); // true setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true
reject()
会实例化一个拒绝的期约并抛出一个异步错误,不能通过try...catch捕获,只能通过拒绝处理程序捕获。
如果给reject传入一个promise,则这个promise会成为返回的拒绝promise的理由。
const p1 = new Promise(() => {}); const p2 = Promise.resolve(111); const r3 = Promise.reject(p1); const r4 = Promise.reject(p2); console.log(r3); console.log(r4);
Promise常用API——all()、allSettled()、any()、race()
all()
假如我们有一个需求,一个页面需要请求3个接口才能渲染,并且要求3个接口必须全部返回。如果我们通过链式调用的方式,接口1请求了再去请求接口2然后去请求接口3,全都成功了再去渲染页面。这种就很耗时,所以就有了一个all的方法来解决。
Promise.all([promise对象,promise对象,...]).then(回调函数)
Promise.all()的参数是一个Promise数组,只有数组中所有的Promise的状态变成了fulfilled
之后才会执行.then回调的第一个回调函数,并且将每个Promise结果的数组变为回调函数的参数。如果Promise中有一个rejected
,那么就会触发.catch()的回调。
let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve("第⼀个promise执⾏完毕"); }, 1000); }); let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve("第⼆个promise执⾏完毕"); }, 2000); }); let p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve("第三个promise执⾏完毕"); }, 3000); }); Promise.all([p1, p3, p2]) .then((res) => { console.log(res); }) .catch(function (err) { console.log(err); }); // 3s后打印 ['第⼀个promise执⾏完毕', '第三个promise执⾏完毕', '第⼆个promise执⾏完毕']
race()
race()方法与all()方法的使用格式相同,不同的是,回调函数的参数是promise数组中最快执行完毕的promise的返回值,它的状态可能是fulfilled
也有可能是rejected
,但是是最快返回的。
根据race这个单词就能理解,相当于一群promise进行比赛,谁先到终点第一就是谁,不管是男是女。
//promise.race()相当于将传⼊的所有任务进行一个竞争 let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve("第⼀个promise执⾏完毕"); }, 5000); }); let p2 = new Promise((resolve, reject) => { setTimeout(() => { reject("第⼆个promise执⾏完毕"); }, 2000); }); let p3 = new Promise((resolve) => { setTimeout(() => { resolve("第三个promise执⾏完毕"); }, 3000); }); Promise.race([p1, p3, p2]) .then((res) => { console.log(res); }) .catch(function (err) { console.error(err); }); // 2秒后打印第二个promise执行完毕
allSettled()
该方法需要传入所有不在pendding状态的promise数组,然后通过该方法可以知道数组中的promise的当前状态。
当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个promise的结果时,通常使用它。
const promise1 = Promise.resolve(3); const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo')); const promises = [promise1, promise2]; Promise.allSettled(promises). then((results) => results.forEach((result) => console.log(result.status))); // "fulfilled" // "rejected"
any()
这个方法目前还是实验性的,不是所有的游览器都能够支持。
接受一个promise数组,只要有一个promise的状态变成了fulfilled
,那么这个方法就会返回这个promise;
如果所有的promise的状态都是rejected
,那么就返回失败的promise,并且把单一的错误集合在一起。
const pErr = new Promise((resolve, reject) => { reject("总是失败"); }); const pSlow = new Promise((resolve, reject) => { setTimeout(resolve, 500, "最终完成"); }); const pFast = new Promise((resolve, reject) => { setTimeout(resolve, 100, "很快完成"); }); Promise.any([pErr, pSlow, pFast]).then((value) => { console.log(value); }) // 很快完成
到此这篇关于深入学习JavaScript中的promise的文章就介绍到这了,更多相关JavaScript promise内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!