文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

在异步编程中,你真的懂Promise吗?

2024-12-24 19:03

关注

在异步编程中,Promise 扮演了举足轻重的角色,比传统的解决方案(回调函数和事件)更合理和更强大。可能有些小伙伴会有这样的疑问:2020年了,怎么还在谈论Promise?事实上,有些朋友对于这个几乎每天都在打交道的“老朋友”,貌似全懂,但稍加深入就可能疑问百出,本文带大家深入理解这个熟悉的陌生人—— Promise。

基本用法

1. 语法

  1. new Promise( function(resolve, reject) {...}   ) 

值得注意的是,Promise 是用来管理异步编程的,它本身不是异步的,new Promise的时候会立即把executor函数执行,只不过我们一般会在executor函数中处理一个异步操作。比如下面代码中,一开始是会先打印出2。

  1. let p1 = new Promise(()=>
  2.     setTimeout(()=>
  3.       console.log(1) 
  4.     },1000) 
  5.     console.log(2) 
  6.   }) 
  7. console.log(3) // 2 3 1 

Promise 采用了回调函数延迟绑定技术,在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。这具体是啥意思呢?我们先来看下面的例子:

  1. let p1 = new Promise((resolve,reject)=>
  2.   console.log(1); 
  3.   resolve('浪里行舟') 
  4.   console.log(2) 
  5. }) 
  6. // then:设置成功或者失败后处理的方法 
  7. p1.then(result=>
  8.  //p1延迟绑定回调函数 
  9.   console.log('成功 '+result) 
  10. },reason=>
  11.   console.log('失败 '+reason) 
  12. }) 
  13. console.log(3) 
  14. // 1 
  15. // 2 
  16. // 3 
  17. // 成功 浪里行舟 

new Promise的时候先执行executor函数,打印出 1、2,Promise在执行resolve时,触发微任务,还是继续往下执行同步任务, 执行p1.then时,存储起来两个函数(此时这两个函数还没有执行),然后打印出3,此时同步任务执行完成,最后执行刚刚那个微任务,从而执行.then中成功的方法。

2. 错误处理

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。具备了这样“冒泡”的特性后,就不需要在每个 Promise 对象中单独捕获异常了。

要遇到一个then,要执行成功或者失败的方法,但如果此方法并没有在当前then中被定义,则顺延到下一个对应的函数:

  1. function executor (resolve, reject) { 
  2.   let rand = Math.random() 
  3.   console.log(1) 
  4.   console.log(rand) 
  5.   if (rand > 0.5) { 
  6.     resolve() 
  7.   } else { 
  8.     reject() 
  9.   } 
  10. var p0 = new Promise(executor) 
  11. var p1 = p0.then((value) => { 
  12.   console.log('succeed-1') 
  13.   return new Promise(executor) 
  14. }) 
  15. var p2 = p1.then((value) => { 
  16.   console.log('succeed-2') 
  17.   return new Promise(executor) 
  18. }) 
  19. p2.catch((error) => { 
  20.   console.log('error', error) 
  21. }) 
  22. console.log(2) 

这段代码有三个 Promise 对象:p0~p2。无论哪个对象里面抛出异常,都可以通过最后一个对象 p2.catch 来捕获异常,通过这种方式可以将所有 Promise 对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题。

通过这种方式,我们就消灭了嵌套调用和频繁的错误处理,这样使得我们写出来的代码更加优雅,更加符合人的线性思维。

3. Promise链式调用

我们都知道可以把多个Promise连接到一起来表示一系列异步骤。这种方式可以实现的关键在于以下两个Promise 固有行为特性:

先通过下面的例子,来解释一下刚刚这段话是什么意思,然后详细介绍下链式调用的执行流程:

  1. let p1=new Promise((resolve,reject)=>
  2.     resolve(100) // 决定了下个then中成功方法会被执行 
  3. }) 
  4. // 连接p1 
  5. let p2=p1.then(result=>
  6.     console.log('成功1 '+result) 
  7.     return Promise.reject(1)  
  8. // 返回一个新的Promise实例,决定了当前实例是失败的,所以决定下一个then中失败方法会被执行 
  9. },reason=>
  10.     console.log('失败1 '+reason) 
  11.     return 200 
  12. }) 
  13. // 连接p2  
  14. let p3=p2.then(result=>
  15.     console.log('成功2 '+result) 
  16. },reason=>
  17.     console.log('失败2 '+reason) 
  18. }) 
  19. // 成功1 100 
  20. // 失败2 1 

我们通过返回 Promise.reject(1) ,完成了第一个调用then创建并返回的promise p2。p2的then调用在运行时会从return Promise.reject(1) 语句接受完成值。当然,p2.then又创建了另一个新的promise,可以用变量p3存储。

new Promise出来的实例,成功或者失败,取决于executor函数执行的时候,执行的是resolve还是reject决定的,或executor函数执行发生异常错误,这两种情况都会把实例状态改为失败的。

p2执行then返回的新实例的状态,决定下一个then中哪一个方法会被执行,有以下几种情况:

我们再来看个例子:

  1. new Promise(resolve=>
  2.     resolve(a) // 报错  
  3. // 这个executor函数执行发生异常错误,决定下个then失败方法会被执行 
  4. }).then(result=>
  5.     console.log(`成功:${result}`) 
  6.     return result*10 
  7. },reason=>
  8.     console.log(`失败:${reason}`) 
  9. // 执行这句时候,没有发生异常或者返回一个失败的Promise实例,所以下个then成功方法会被执行 
  10. // 这里没有return,最后会返回 undefined 
  11. }).then(result=>
  12.     console.log(`成功:${result}`) 
  13. },reason=>
  14.     console.log(`失败:${reason}`) 
  15. }) 
  16. // 失败:ReferenceError: a is not defined 
  17. // 成功:undefined 

4. async & await

从上面一些例子,我们可以看出,虽然使用 Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程。

ES7中新增的异步编程方法,async/await的实现是基于 Promise的,简单而言就是async 函数就是返回Promise对象,是generator的语法糖。很多人认为async/await是异步操作的终极解决方案:

不过也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

  1. async function test() { 
  2.   // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式 
  3.   // 如果有依赖性的话,其实就是解决回调地狱的例子了 
  4.   await fetch(url1) 
  5.   await fetch(url2) 
  6.   await fetch(url3) 

观察下面这段代码,你能判断出打印出来的内容是什么吗?

  1. let p1 = Promise.resolve(1) 
  2. let p2 = new Promise(resolve => { 
  3.   setTimeout(() => { 
  4.     resolve(2) 
  5.   }, 1000) 
  6. }) 
  7. async function fn() { 
  8.   console.log(1) 
  9. // 当代码执行到此行(先把此行),构建一个异步的微任务 
  10. // 等待promise返回结果,并且await下面的代码也都被列到任务队列中 
  11.   let result1 = await p2 
  12.   console.log(3) 
  13.   let result2 = await p1 
  14.   console.log(4) 
  15. fn() 
  16. console.log(2) 
  17. // 1 2 3 4 

如果 await 右侧表达逻辑是个 promise,await会等待这个promise的返回结果,只有返回的状态是resolved情况,才会把结果返回,如果promise是失败状态,则await不会接收其返回结果,await下面的代码也不会在继续执行。

  1. let p1 = Promise.reject(100) 
  2. async function fn1() { 
  3.   let result = await p1 
  4.   console.log(1) //这行代码不会执行 

我们再来看道比较复杂的题目:

  1. console.log(1) 
  2. setTimeout(()=>{console.log(2)},1000) 
  3. async function fn(){ 
  4.     console.log(3) 
  5.     setTimeout(()=>{console.log(4)},20) 
  6.     return Promise.reject() 
  7. async function run(){ 
  8.     console.log(5) 
  9.     await fn() 
  10.     console.log(6) 
  11. run() 
  12. //需要执行150ms左右 
  13. for(let i=0;i<90000000;i++){} 
  14. setTimeout(()=>
  15.     console.log(7) 
  16.     new Promise(resolve=>
  17.         console.log(8) 
  18.         resolve() 
  19.     }).then(()=>{console.log(9)}) 
  20. },0) 
  21. console.log(10) 
  22. // 1 5 3 10 4 7 8 9 2 

做这道题之前,读者需明白:

接下来,我们一步一步分析:

常用的方法

1. Promise.resolve()

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。Promise.resolve()等价于下面的写法:

  1. Promise.resolve('foo') 
  2. // 等价于 
  3. new Promise(resolve => resolve('foo')) 

Promise.resolve方法的参数分成四种情况。

(1)参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  1. const p1 = new Promise(function (resolve, reject) { 
  2.   setTimeout(() => reject(new Error('fail')), 3000) 
  3. }) 
  4. const p2 = new Promise(function (resolve, reject) { 
  5.   setTimeout(() => resolve(p1), 1000) 
  6. }) 
  7. p2 
  8.   .then(result => console.log(result)) 
  9.   .catch(error => console.log(error)) 
  10. // Error: fail 

上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

(2)参数不是具有then方法的对象,或根本就不是对象

  1. Promise.resolve("Success").then(function(value) { 
  2.  // Promise.resolve方法的参数,会同时传给回调函数。 
  3.   console.log(value); // "Success" 
  4. }, function(value) { 
  5.   // 不会被调用 
  6. }); 

(3)不带有任何参数

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

  1. Promise.resolve().then(function () { 
  2.   console.log('two'); 
  3. }); 
  4. console.log('one'); 
  5. // one two 

(4)参数是一个thenable对象

thenable对象指的是具有then方法的对象,Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

  1. let thenable = { 
  2.   then: function(resolve, reject) { 
  3.     resolve(42); 
  4.   } 
  5. }; 
  6. let p1 = Promise.resolve(thenable); 
  7. p1.then(function(value) { 
  8.   console.log(value);  // 42 
  9. }); 

2. Promise.reject()

Promise.reject()方法返回一个带有拒绝原因的Promise对象。

  1. new Promise((resolve,reject) => { 
  2.     reject(new Error("出错了")); 
  3. }); 
  4. // 等价于 
  5.  Promise.reject(new Error("出错了"));   
  6.  
  7. // 使用方法 
  8. Promise.reject(new Error("BOOM!")).catch(error => { 
  9.     console.error(error); 
  10. }); 

值得注意的是,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

  1. new Promise((resolve, reject) => { 
  2.   return reject(1); 
  3.   // 后面的语句不会执行 
  4.   console.log(2); 
  5. }) 

3. Promise.all()

  1. let p1 = Promise.resolve(1) 
  2. let p2 = new Promise(resolve => { 
  3.   setTimeout(() => { 
  4.     resolve(2) 
  5.   }, 1000) 
  6. }) 
  7. let p3 = Promise.resolve(3) 
  8. Promise.all([p3, p2, p1]) 
  9.   .then(result => { 
  10.  // 返回的结果是按照Array中编写实例的顺序来 
  11.     console.log(result) // [ 3, 2, 1 ] 
  12.   }) 
  13.   .catch(reason => { 
  14.     console.log("失败:reason") 
  15.   }) 

Promise.all 生成并返回一个新的 Promise 对象,所以它可以使用 Promise 实例的所有方法。参数传递promise数组中所有的 Promise 对象都变为resolve的时候,该方法才会返回, 新创建的 Promise 则会使用这些 promise 的值。

如果参数中的任何一个promise为reject的话,则整个Promise.all调用会立即终止,并返回一个reject的新的 Promise 对象。

4. Promise.allSettled()

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,ES2020 引入Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

假如有这样的场景:一个页面有三个区域,分别对应三个独立的接口数据,使用 Promise.all 来并发请求三个接口,如果其中任意一个接口出现异常,状态是reject,这会导致页面中该三个区域数据全都无法出来,显然这种状况我们是无法接受,Promise.allSettled的出现就可以解决这个痛点:

  1. Promise.allSettled([ 
  2.   Promise.reject({ code: 500, msg: '服务异常' }), 
  3.   Promise.resolve({ code: 200, list: [] }), 
  4.   Promise.resolve({ code: 200, list: [] }) 
  5. ]).then(res => { 
  6.   console.log(res) 
  7.    
  8.   // 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染 
  9.   RenderContent( 
  10.     res.filter(el => { 
  11.       return el.status !== 'rejected' 
  12.     }) 
  13.   ) 
  14. }) 

Promise.allSettled跟Promise.all类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 它不会进行短路, 也就是说当Promise全部处理完成后,我们可以拿到每个Promise的状态, 而不管是否处理成功。

5. Promise.race()

Promise.all()方法的效果是"谁跑的慢,以谁为准执行回调",那么相对的就有另一个方法"谁跑的快,以谁为准执行回调",这就是Promise.race()方法,这个词本来就是赛跑的意思。race的用法与all一样,接收一个promise对象数组为参数。

Promise.all在接收到的所有的对象promise都变为FulFilled或者Rejected状态之后才会继续进行后面的处理,与之相对的是Promise.race只要有一个promise对象进入FulFilled或者Rejected状态的话,就会继续进行后面的处理。

  1. // `delay`毫秒后执行resolve 
  2. function timerPromisefy(delay) { 
  3.     return new Promise(resolve => { 
  4.         setTimeout(() => { 
  5.             resolve(delay); 
  6.         }, delay); 
  7.     }); 
  8. // 任何一个promise变为resolve或reject的话程序就停止运行 
  9. Promise.race([ 
  10.     timerPromisefy(1), 
  11.     timerPromisefy(32), 
  12.     timerPromisefy(64) 
  13. ]).then(function (value) { 
  14.     console.log(value);    // => 1 
  15. }); 

上面的代码创建了3个promise对象,这些promise对象会分别在1ms、32ms 和 64ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后,.then注册的回调函数就会被调用。

6. Promise.prototype.finally()

ES9 新增 finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()和catch()中各写一次的情况。

比如我们发送请求之前会出现一个loading,当我们请求发送完成之后,不管请求有没有出错,我们都希望关掉这个loading。

  1. this.loading = true 
  2. request() 
  3.   .then((res) => { 
  4.     // do something 
  5.   }) 
  6.   .catch(() => { 
  7.     // log err 
  8.   }) 
  9.   .finally(() => { 
  10.     this.loading = false 
  11.   }) 

finally方法的回调函数不接受任何参数,这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

实际应用

假设有这样一个需求:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?三个亮灯函数已经存在:

  1. function red() { 
  2.     console.log('red'); 
  3. function green() { 
  4.     console.log('green'); 
  5. function yellow() { 
  6.     console.log('yellow'); 

这道题复杂的地方在于需要“交替重复”亮灯,而不是亮完一遍就结束的一锤子买卖,我们可以通过递归来实现:

  1. // 用 promise 实现 
  2. let task = (timer, light) => { 
  3.   return new Promise((resolve, reject) => { 
  4.     setTimeout(() => { 
  5.       if (light === 'red') { 
  6.         red() 
  7.       } 
  8.       if (light === 'green') { 
  9.         green() 
  10.       } 
  11.       if (light === 'yellow') { 
  12.         yellow() 
  13.       } 
  14.       resolve() 
  15.     }, timer); 
  16.   }) 
  17. let step = () => { 
  18.   task(3000, 'red') 
  19.     .then(() => task(1000, 'green')) 
  20.     .then(() => task(2000, 'yellow')) 
  21.     .then(step) 
  22. step() 

同样也可以通过async/await 的实现:

  1. //  async/await 实现 
  2. let step = async () => { 
  3.   await task(3000, 'red') 
  4.   await task(1000, 'green') 
  5.   await task(2000, 'yellow') 
  6.   step() 
  7. step() 

使用 async/await 可以实现用同步代码的风格来编写异步代码,毫无疑问,还是 async/await 的方案更加直观,不过深入理解Promise 是掌握async/await的基础。 

 

来源:前端工匠内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯