JavaScript promise
陈梵阿 人气:0为什么要用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); }) // 很快完成
加载全部内容