Promise精简版
一、为什么会出现promise?
为什么会出现promise?
promise的出现是为了解决JavaScript中异步编程问题,特别是回调地狱(ajax嵌套),Ajax产生的回调地狱,就是当异步操作发生嵌套时,第二个Ajax会依赖于上一个Ajax的请求,前一个Ajax的请求未回来,它就不会执行,就会造成代码的拥堵。
为什么promise能够解决回调地狱?promise的底层原理是啥?
promise之所以能够解决回调地狱,主要是因为内部机制和链式调用机制,链式调用机制就是一个promise调用多个.then()方法,每个.then()
方法都会返回一个新的Promise对象,并且这些.then()
方法是同时执行的,不会相互阻塞。新的Promise对象的状态会根据前一个Promise的状态来决定自己的状态,这是Promise链的核心机制。当前一个Promise完成(fulfilled)时,下一个.then()
方法会执行,如果前一个Promise被拒绝(rejected),则会执行相应的错误处理(通常是 .catch()
方法)。这是非常有用的,因为它使得代码可以按照逻辑顺序执行异步操作,而不需要嵌套回调。
在Promise链中,每个.then()
方法都会执行,但每个.then()
方法返回的新Promise对象的状态会根据前一个Promise的状态来决定自己的状态。如果任何一个Promise被拒绝(rejected),那么整个链上的后续.then()
方法都会被跳过,直接进入错误处理,通常是.catch()
方法。这是Promise链的错误处理机制,确保错误能够被捕获和处理,而不会继续执行成功处理的回调。这是Promise链中非常重要的行为,有助于更好地管理异步操作和错误情况。
二、Promise是什么?
Promise提供了一种更可读、可维护和可控的方式来处理异步代码,允许开发者更清晰地表示异步操作的状态和结果,同时提供了一致的接口,从而改善了异步编程的质量和可维护性。它使异步操作的代码变得更加结构化,降低了复杂性,并提供了更好的错误管理,让代码更容易理解和维护。
关键特点和含义:
- 状态: Promise可以处于三种状态之一:进行中(pending)、已完成(fulfilled)、已拒绝(rejected)。这反映了异步操作的进展。
- 状态转换: Promise的状态从进行中可以转变为已完成或已拒绝,但一旦状态发生转换,就不能再次改变。
- 异步操作: Promise通常用于包装异步操作,例如Ajax请求、文件读取等。
- 链式调用: Promise支持链式调用,允许您按顺序组织和处理异步操作,而不需要深度嵌套回调函数,从而避免回调地狱问题。
- 错误处理: Promise提供了
.catch()
方法,用于捕获和处理异步操作中的错误,这提供了更好的错误处理机制。
2.1 Promise的初体验
创建promise对象(pending状态)
const p = new Promise(executor);
其中:
executor函数: 执行器 (resolve, reject) => {}
resolve函数: 内部定义成功时我们调用的函数 value => {}
reject函数: 内部定义失败时我们调用的函数 reason => {}
executor会在Promise内部立即同步调用,异步操作在执行器中执行
实例对象调用Promise原型中的then方法来完成对结果的处理
1 | <script> |
拓展版本
1 | <script> |
三、使用Promise的好处?
3.1 指定回调函数的方式更加灵活
旧的:必须在启动异步任务前指定
promise:启动异步任务->返回promise对象->给promise对象绑定回调函数
(甚至可以在异步任务结束后指定/多个)
3.2 可以解决回调地狱问题,支持链式调用
什么是回调地狱?
回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件
回调地狱的缺点?
不便于阅读
不便于异常处理
不便于管理
解决方案?
promise链式调用
终极解决方案?
async/await
async
函数: async
函数是一种特殊的函数,它返回一个 Promise
。在 async
函数内部,您可以使用 await
关键字来等待 Promise
的解决(fulfilled)或拒绝(rejected)
async 函数返回 Promise: 当您声明一个函数为async
函数,它将自动返回一个Promise
对象。这意味着async
函数内的操作可以异步执行,而函数本身会返回一个Promise
,其状态将根据async
函数内的执行结果而定。
await
关键字: await
用于等待一个 Promise
对象的解决。当在 async
函数内部使用 await
时,函数会暂停执行,直到等待的 Promise
解决为止。这使得代码看起来更像同步代码,而不是回调或链式的 Promise
调用。
await 等待 Promise: await
是async/await
中的关键字,用于等待一个Promise
对象的状态变化。当您在async
函数中使用await
时,函数会暂停执行,直到等待的Promise
状态变为已解决(fulfilled)或拒绝(rejected)。这使得异步操作看起来更像同步代码,提高了可读性
1 | const promise = new Promise((resolve, reject) => { |
1 | async function myAsyncFunction() { |
四、Promise实例对象的两个属性
PromiseState
此属性为promise对象的状态属性。
- fulfilled:成功的状态(已成功)
- rejected:失败的状态(已拒绝)
- pending:初始化的状态(已完成)
【注】状态只能由pending->fulfilled 或者是 pending->rejected
PromiseResult
此属性为promise对象的结果值(resolve以及reject函数的形参值)
总结:
两个属性:状态属性和结果值属性
使用状态属性(PromiseState
)来判断 Promise
当前的状态,然后根据状态再访问结果值属性(PromiseResult
)来获取相应的值或原因。这样可以更准确地处理 Promise
的不同状态和结果。
五、resolve函数以及reject函数
- resolve:修改promise对象的状态,由pending修改到fulfilled;将实参设置到这个属性PromiseResult中。
- reject:修改promise对象的状态,由pending修改到rejected;将实参设置到这个属性PromiseResult中。
resolve函数以及reject函数是promise中的两个函数,用来控制状态和最终结果,其中resolve函数是成功,reject函数是失败,resolve函数被调用的时,会把值交给下一个.then()方法生成的promise对象去处理,当调用reject函数时,promise对象会进入已拒绝状态,它会将值传递给最后一个.catch()方法去处理,.then() 方法会一直执行,不会被跳过。但是,如果前一个 Promise 处于已拒绝状态,它会跳过前一个 Promise 已完成状态的成功回调(第一个参数),而直接执行失败回调(第二个参数)或 .catch() 方法。
1 | // 创建一个 Promise 对象,使用 resolve 函数表示异步操作成功 |
1 | // 创建一个 Promise 对象,使用 reject 函数表示异步操作失败 |
六、Promise对象的状态
Promise对象通过自身的状态来控制异步操作,Promise实例具有三种状态.
- 异步操作未完成:pending (已准备)
- 异步操作成功:fulfilled (已完成)
- 异步操作失败:rejected (已拒绝)
这三种的状态的变化途径只有两种
- 从pending(未完成)到fulfilled(成功)
- 从pending(未成功)到rejected(失败)
一旦状态发生变化,就凝固了,不会再有新的状态变化,这也是Promise这个名字的由来,它的英语意思”承诺”,
一旦承诺生效,就不得再改变了,这也意味着Promise实例的状态变化只可能发生一次。
在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。
上面的resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。
因此,Promise的最终结果只有两种。
1 | 异步操作成功,Promise实例传回一个值(value),状态变为fulfilled. |
七、Promise的then方法
then
方法返回一个新的 Promise 对象,这个新的 Promise 的状态和值由 then
方法中的回调函数来决定。
八、Promise的链式调用
Promise 的链式调用是指在一个 Promise 上连续调用多个 then
方法,形成一个链式结构,以便处理异步操作的结果和顺序操作。
当你在一个 Promise 上多次调用 then
方法时,每个 then
方法会返回一个新的 Promise,这个新 Promise 会立即进入 pending 状态,然后根据前一个 then
方法的状态和返回值来确定自己的状态。
然多个 then
方法的新 Promise 可能同时进入 pending 状态,但它们的执行顺序是连续的,并且依赖于前一个 Promise 的状态和前一个 then
的回调函数的结果。这种连续的执行顺序使得 Promise 能够以有序的方式处理异步操作的结果,增加了代码的可读性和可维护性。
九、Promise下的几种方法
十、终止Promise链条
在 Promise 中,通常情况下,Promise 链是一个连续的操作序列,每个 Promise 的成功状态都会触发下一个 Promise 的执行。但有时候,可能需要在某个特定条件下终止 Promise 链,不再执行后续的操作。有几种方法可以实现终止 Promise 链。
在 Promise 链中,可以使用 return
语句来提前退出链条。当then
或 catch
在 回调中使用 return
语句时,它将阻止后续的 then
或 catch
回调被执行。
1 | promise |
可以在 then
或 catch
回调中使用 throw
来抛出一个异常,从而拒绝当前 Promise,终止链。
1 | promise |
可以在某个条件下直接返回一个被拒绝的 Promise,以终止链。
1 | const condition = true; |
十一、几个关键问题
11.1 如何修改 promise 对象状态
promise的状态只能修改一次,从pending到reject或者resolve,状态改变之后,就不能再次修改,没有后悔的权利。这也是promise的一个核心特性,叫做不可逆性
11.2 指定多个回调执行情况
问题:一个promise指定多个成功/失败回调函数,都会调用吗?
多个成功或失败回调函数都会被调用,但它们的执行顺序和结果会受到上一个 .then()
方法返回的 Promise 对象的状态和值影响。
如果前一个 Promise 状态是 fulfilled
(已成功),那么所有的成功回调函数将按照它们添加到 Promise 链的顺序执行,并且它们将依次接收相同的成功值。如果前一个 Promise 状态是 rejected
(已拒绝),那么所有的失败回调函数将按照它们添加到 Promise 链的顺序执行,同时依次接收相同的拒绝原因。
11.3 指定回调与改变状态先后顺序问题
改变promise状态和指定回调函数执行谁先谁后?
首选从底层逻辑来说,是先指定回调函数再改变promise状态,之所以是这个顺序,是因为队列优先级不一样,promise状态走的是事件队列,指定回调函数走的是微任务队列(也可以叫做vip通道),所指定回调函数会先到,然后等待promise状态走完,所以就造成指定回调函数先执行,promise状态后执行的情况。 无法人为修改这个顺序,但是可以通过一些方法来影响这个顺序,第一种是使用.all方法,.all方法会等所有promise状态走完才会触发回调,就相当于.all阻止了回调函数进入微服务队列,只有等promise状态走完,才会让它进入微服务队列,但是这个时候,就算他走的是高优先级的队列也无济于事,顺序已经被影响了。第二种办法是在执行器中直接调resolve和reject这种办法可以直接暴力的修改promise的状态,比之指定回调函数走的微服务队列更快。第三种方法是直接对回调函数出手,给回调函数加个定时器,延缓.then方法的调用。
11.4 promise.then()返回的新promise的结果状态由什么决定?
- 简单表达:由then指定的回调函数执行的结果决定
- 详细表达:
- 如果抛出异常:新promise对象状态变成rejected,reason为抛出的异常
- 如果返回的是是非promise的任意值,新promise对象状态变成fulfilled,value为返回的值
- 如果返回的是另一个新的promise对象,此promise的结果就会称为新promise的结果
11.5 promise如何串联多个操作任务?
- promise的then()返回一个新的promise对象,可以写成then()方法的链式调用
- 通过then()的链式调用串联多个同步/异步任务
11.6 promise的异常穿透
在 Promise 链中,每个 .then()
方法都会返回一个新的 Promise 对象。如果其中一个 .then()
方法中出现异常(例如,.then()
方法中的回调函数抛出了错误),那么这个异常会直接传播到接下来的 .catch()
方法,而不会走到 .try()
方法。
这种异常穿透的机制确实提高了异常的处理效率,因为异常会在 Promise 链中快速传递并在适当的位置得到处理。这也使得我们可以在 .catch()
方法中决定如何处理异常,包括是否继续执行后续的操作(使用 try
)。这样的控制流程有助于处理异步操作中的异常情况,确保异常不被忽略。
11.7 中断promise链
promise链的中断,中断promise链,其实本质上就是这个特殊的异常直接传到最后的.catch(),比异常穿透还要暴力,异常穿透还得走每一个then方法的catch,依次往下找,中断promise就比较狠,直接传到最后那个catch。中断promise有以下几种方法,第一种方法是使用条件控制,所谓的条件控制就是在then方法内部,可以选择不执行( return result;),也可以返回一个resolved Promise 来中断执行。第二种方法就是使用Promise.race();来定义一个包含resolve和reject的promise数组来等待指定的promise完成,一旦完成,直接中断
十二、async和await
async
是一个关键字,用于定义异步函数。异步函数返回一个 Promise 对象,它允许在函数内执行异步操作,使代码更具可读性和可维护性。await
也是一个关键字,通常用于等待 Promise 对象的解析结果。当使用await
关键字时,函数会暂停执行,直到该 Promise 被 resolved(完成)或 rejected(拒绝)。await
创建的是一个 Promise 对象,其本质和.then
类似,但语法更简洁。async
函数内部可以包含多个await
表达式,这使得多个异步操作可以按照顺序执行,非常适合处理复杂的异步任务流程。async
和await
使异步代码看起来更像同步代码,减少了回调地狱(Callback Hell)问题,提高了可读性和可维护性。
十三、JS中的宏队列与微队列
从整体上来说,它两都是队列,所以都是先进先出的执行顺序,从名字也能看出来,宏队列要比微队列大很多,宏队列是用来存储比较大并且复杂的任务,微队列是用来存储相对偏小且比较简单的任务,虽然两者走的都是异步操作,但是微队列的优先级要比宏队列高,就好比vip通道要比普通通道快,实际开发中,宏队列和微队列会进行适当的结合使用,微队列一般用来观察或监视宏队列,搭配使用,可以提高效率。
十四、Promise常见面试题
1 | setTimeout(() => { |
分析:首先是一个定时器,会把这个任务放到宏队列里面去,第二个和第三个是成功的回调,是指定回调函数,会放入微队列中,最后一个是直接输出,相对于前三个的队列来说,第四个是最快的,其次是微队列,队列都遵循先进先出的执行顺序,所以先进去的先执行,又因为微队列的优先级高于宏队列,所以宏队列排最后,综上所述,输出顺序是:直接输出>微队列(先进先出)>宏队列,输出结果是3241
1 | setTimeout(() => { |
分析:从整体来看,也就分为三部分,宏队列、微队列、直接输出。我们先来看直接输出,当创建一个 Promise 对象时,构造函数内的代码会立即执行,所以相当于直接输出。但是 resolve()
方法会将 Promise 的状态从 pending 改变为 resolved,并且安排后续的 .then()
方法回调在微任务队列中执行。最后那个打印5也是直接输出,所以直接输出的输出结果是2,5。直接输出分析完了,现在来分析微队列,微队列其实也就是两个then()方法,但是由于promise是链式调用结合状态的改变,虽然一开始两个then方法生成的promise的新对象都是pending状态,但是之后状态的改变要等上一个promise的状态改变之后才会进行相对应的改变,所以第一个then方法执行完毕之后,第二个then方法才会去执行,所以微队列中的输出结果是3,4。最后来分析宏队列,只有一个宏队列,所以不用分析,直接最后输出的就是1。最终的输出结果是2,5,3,4,1
1 | const first = () => (new Promise((resolve, reject) => { |
分析:整体来说先从同步和异步分析,同步优先于异步,先看同步,从上往下看的话,依次先输出3、7、4
,然后看异步,首先是first函数创建promise对象,在frist函数外部进行调用,里面是一个p的promise对象,在first函数里面被调用,这是一个大概的组成分析,现在往里面来进行详细分析,p函数内部的第一个异步是setTimeout是一个宏队列,先将其放到一边,先分析微队列,然后往下是resolve(1),说明p的这个promise对象立马完成,往下继续看,resolve(2)对应的是first函数里面的这个promise完成,接着p.then(),p被调用,这个指定回调会进入微队列,往外看,first().then()也被调用,所对应的指定回调也会进入微队列,又因为队列的先进先出,所有微队列的输出是1,2
。最后看宏队列,因为只有一个,所以也不用分析了,就是5
。最后就是执行宏队列里面的resolve(6),可是执行也没用,promise的状态只能变一次,而且是不可逆性的,所以这一行代码没有实际意义。最终的输出结果是3、7、4、1、2、5
答案:
console.log(3)
和console.log(7)
是同步任务,它们会在第一个微任务队列之前输出。所以,输出顺序是3, 7
。console.log(4)
也是同步任务,直接输出。所以,输出是3, 7, 4
。- 接下来是异步任务:
setTimeout(() => { console.log(5); resolve(6) }, 0)
。这个任务会被添加到宏队列中,但由于宏队列中的任务需要等待微任务队列执行完毕,所以此时不会立即执行。所以console.log(5)
不会在这一阶段输出。 resolve(1)
和resolve(2)
是在两个不同的 Promise 中,但它们都会立即执行,将对应的 Promise 从 pending 状态改变为 resolved。这会触发微任务队列中的回调函数。因此,输出是3, 7, 4, 1, 2
。p.then((arg) => { console.log(arg) })
中的回调函数会在微任务队列中执行。因此,输出是3, 7, 4, 1, 2, 5
。请注意,resolve(6)
并不会改变 Promise 的状态,因为 Promise 的状态一旦变为 resolved 或 rejected,就无法再次改变。
所以,最终的输出顺序应该是 3, 7, 4, 1, 2, 5
,而 resolve(6)
确实没有实际意义。
1 | setTimeout(() => { |
分析:同步任务:console.log(“1”),console.log(“7”),至于为什么console.log(“3”)不是,那是因为他是异步里面的同步任务,所以先输出1,7
。
微队列:首先是第一个promise的第一个then方法,将其放入微队列,然后看第二个promise的then方法,也将其放入到微队列里面。然后往里面看,第一个promise的then方法执行console.log(“2”)输出2
,往下继续看,在里面还创建了一个promise对象,首先是一个内嵌的同步任务,所以执行console.log(“3”),输出3
,接着就是这个promise对象的then方法的回调,将其放入微队列中。因为第一个promise的第一个.then有结果了,所以微队列中这个方法已经执行完毕,微任务队列中的下一个任务是外部 Promise 的 then 函数(console.log(“6”)),只有当外部 Promise 的 then函数执行完毕后,才会执行内部 Promise 的 then 函数,接着看微队列里面的第二个promise的then,执行console.log(“8”)输出8
,此时看队列里面的第三个任务,也就是第一个promise里面的promise对象的then,输出4
,同上理可得第二个.then不会马上执行,然后再从上往下看,先是第一个promise的的第二个.then,执行console.log(“6”);输出6
。接着是第一个promise的内嵌promise的第二个then,执行 console.log(“5”)输出5
,所以微队列的输出是:2,3,8,4,6,5
宏队列:一个宏队列任务,那个定时器,所以不用分析,宏队列执行 console.log(“0”)输出0
,
综上所述,最终的输出结果是:1,7,2,3,8,4,6,5,0