【前端】从小白视角上手Promise、Async/Await和手撕代码

开发 前端
对于前端新手而言,Promise是一件比较困扰学习的事情,需要理解的细节比较多。对于前端面试而言,Promise是面试官最常问的问题,特别是手撕源码。众所周知,JavaScript语言执行环境是“单线程”。

[[400035]]

写在前面

对于前端新手而言,Promise是一件比较困扰学习的事情,需要理解的细节比较多。对于前端面试而言,Promise是面试官最常问的问题,特别是手撕源码。众所周知,JavaScript语言执行环境是“单线程”。

单线程,就是指一次只能完成一件任务,如果有多个任务就必须排队等候,前面一个任务完成,再执行后面一个任务。这种“单线程”模式执行效率较低,任务耗时长。

为了解决这个问题,就有了异步模式,也叫异步编程。

一、异步编程

所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,当第一段有了执行结果之后,再回过头执行第二段。

JavaScript采用异步编程原因有两点:

  • JavaScript是单线程。
  • 为了提高CPU的利用率。

在提高CPU的利用率的同时也提高了开发难度,尤其是在代码的可读性上。

那么异步存在的场景有:

  • fs 文件操作
  1. require("fs").readFile("./index.html",(err,data)=>{}) 
  • 数据库操作
  • AJAX
  1. $.get("/user",(data)=>{}) 

定时器

  1. setTimeout(()=>{},2000) 

二、Promise是什么

Promise理解

(1) 抽象表达

  • Promise 是一门新的技术(es6规范)
  • Promise是js中进行异步编程的新解决方案

(2) 具体表达

  • 从语法上说:Promise是一个构造函数
  • 从功能上说:Promise对象是用来封装一个异步操作并可以获取其成功/失败的结果值

为什么要使用Promise

(1) 指定回调函数的方式更加灵活

  • promise:启动异步任务=>返回promise对象=>给promise对象绑定回调函数

(2) 支持链式调用方式,可以解决回调地狱问题

  • 什么是回调地狱?

回调地狱就是回调函数嵌套使用,外部回调函数异步执行的结果是嵌套的回调执行的条件

  • 回调地狱的缺点

不便于阅读

不便于异常处理

解决方法

Promise的状态

  1. Promise必须拥有三种状态:pending、rejected、resolved
  2. 如果Promise的状态是pending时,它可以变成成功fulfilled或失败rejected
  3. 如果promise是成功状态,则它不能转换为任何状态,而且需要一个成功的值,并且这个值不能改变
  4. 如果promise是失败状态,则它不能转换成任何状态,而且需要一个失败的原因,并且这个值不能改变

Promise的状态改变

pending未决定的,指的是实例状态内置的属性

(1)pending变为resolved/fullfilled

(2)pending变为rejected

说明:Promise的状态改变只有两种,且一个Promise对象只能改变一次,无论失败还是成功都会得到一个结果输出,成功的结果一般是value,失败的结果一般是reason。

无论状态是成功还是失败,返回的都是promise。

Promise的值

实例对象中的另一个属性 [PromiseResult]保存着异步任务 [成功/失败] 的结果resolve/reject。

Promise的api

手写Promide中的api:

(1)promise构造函数 Promise(executor){}

  • executor:执行器(resolve,reject)=>{}
  • resolve:内部定义成功时我们需要调用的函数value=>{}
  • reject:内部定义失败时我们调用的函数 reason=>{}说明:executor会在Promise内部立即同步调用,异步操作在执行器中执行

(2)Promise.prototype.then方法:(onResolved,rejected)=>{}

  • onResolved函数:成功的回调函数value=>{}
  • rejected函数:失败的回调函数reason=>{}

说明:指定用于得到成功value的成功回调和用于得到失败reason的失败回调,返回一个新的promise对象

(3)Promise.prototype.catch方法:(onRejected)=>{}

前三条是本文章中将要实现的手写代码,当然Promise还有其它的api接口。

(1)Promise.prototype.finally()方法

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

  1. promise 
  2. .then(result => {···}) 
  3. .catch(error => {···}) 
  4. .finally(() => {···}); 

(2)Promise.all()方法

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

  1. const p = Promise.all([p1, p2, p3]); 

p的状态由p1、p2、p3决定,分成两种情况。

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

(3)Promise.race()方法

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

  1. const p = Promise.race([p1, p2, p3]); 

只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

(4)Promise.allSettled()方法

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

  1. const promises = [ 
  2.   fetch('/api-1'), 
  3.   fetch('/api-2'), 
  4.   fetch('/api-3'), 
  5. ]; 
  6.  
  7. await Promise.allSettled(promises); 
  8. removeLoadingIndicator(); 

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

(5)Promise.any()方法

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

(6)Promise.reject(reason)方法

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

  1. const p = Promise.reject('出错了'); 
  2. // 等同于 
  3. const p = new Promise((resolve, reject) => reject('出错了')) 
  4.  
  5. p.then(nullfunction (s) { 
  6.   console.log(s) 
  7. }); 
  8. // 出错了 

(7)Promise.resolve()方法 有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

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

改变promsie状态和指定回调函数谁先谁后?

(1)都有可能,正常情况下是先指定回调函数再改变状态,但也可以先改变状态再指定回调函数。

(2)如何改变状态再指定回调?

  • 在执行器中直接调用resolve/reject
  • 延迟更长时间才进行调用then

(3)什么时候才能得到数据?

  • 如果先指定的回调,那当状态发生改变,回调函数就会调用,得到数据
  • 如果先改变的状态,那当指定回调时,回调函数就会进行调用,得到数据

示例:

  1. let p = new Promise((resolve,reject)=>{ 
  2.     resolve("成功了"); 
  3.     reject("失败了"); 
  4. }); 
  5.  
  6. p.then((value)=>{ 
  7.     console.log(value); 
  8. },(reason)=>{ 
  9.     console.log(reason); 
  10. }) 

Promise规范

  1. "Promise"是一个具有then方法的对象或函数,其行为符合此规范。也就是说Promise是一个对象或函数
  2. "thenable"是一个具有then方法的对象或函数,也就是这个对象必须拥有then方法
  3. "value"是任何合法的js值(包括undefined或promise)
  4. promise中的异常需要使用throw语句进行抛出
  5. promise失败的时候需要给出失败的原因

then方法说明

  1. 一个promise必须要有一个then方法,而且可以访问promise最终的结果,成功或者失败的值
  2. then方法需要接收两个参数,onfulfilled和onrejected这两个参数是可选参数
  3. promise无论then方法是否执行完毕,只要promise状态变了,then中绑定的函数就会执行。

链式调用

Promise最大的优点就是可以进行链式调用,如果一个then方法返回一个普通值,这个值就会传递给下一次的then中,作为成功的结果。

如果返回的是一个promise,则会把promise的执行结果传递下去取决于这个promise的成功或失败。

如果返回的是一个报错,就会执行到下一个then的失败函数中。

三、手写Promise代码

面试经常考的手写Promise代码,可以仔细理解一下。

  1. // 手写Promise 
  2. // 首先定义一个构造函数,在创建Promise对象的时候会传递一个函数executor, 
  3. // 这个函数会立即被调用,所以我们在Promise内部立即执行这个函数。 
  4. function Promise(executor){ 
  5.     // 用于保存promise的状态 
  6.     this.status = "pending"
  7.     this.value;//初始值 
  8.     this.reason;//初始原因 
  9.     this.onResolvedCallbacks = [];//存放所有成功的回调函数 
  10.     this.onRejectedCallbacks = [];//存放所有失败的回调函数 
  11.     //定义resolve函数 
  12.     const resolve = (value)=>{ 
  13.         if(this.status === "pending"){ 
  14.             this.status = "resolved"
  15.             this.value = value; 
  16.             this.onResolvedCallbacks.forEach(function(fn){ 
  17.                 fn() 
  18.             }) 
  19.         } 
  20.     } 
  21.     //定义reject函数 
  22.     const reject = (reason)=>{ 
  23.         if(this.status === "pending"){ 
  24.             this.status = "rejected"
  25.             this.reason = reason; 
  26.             this.onRejectedCallbacks.forEach(function(fn){ 
  27.                 fn() 
  28.             }) 
  29.         } 
  30.     } 
  31.     executor(resolve,reject); 
  32.  
  33. Promise.prototype.then = function(onFulfilled,onRejected){ 
  34. /* 
  35. 每次then都会返回一个新的promise 
  36.  
  37. 我们需要拿到当前then方法执行成功或失败的结果, 
  38. 前一个then方法的返回值会传递给下一个then方法, 
  39. 所以这里我们要关心onFulfilled(self.value) 
  40. 和 onRejected(self.reason)的返回值,我们这里定义一个x来接收一下。 
  41.  
  42. 如果失败抛错需要执行reject方法,这里使用try...catch捕获一下错误。 
  43. 也就是判断then函数的执行结果和返回的promise的关系。 
  44. */ 
  45.     return new Promise((resolve,reject)=>{ 
  46.         //当Promise状态为resolved时 
  47.         if(this.status === "resolved"){ 
  48.             try{ 
  49.                 resolve(onFulfilled(this.value)) 
  50.             }catch(error){ 
  51.                 reject(error) 
  52.             } 
  53.         } 
  54.         //当Promise状态为rejected时 
  55.         if(this.status === "rejected"){ 
  56.             try { 
  57.                 resolve(onRejected(this.reason)) 
  58.             } catch (error) { 
  59.                 reject(error) 
  60.             } 
  61.         } 
  62.         //当Promise状态为pendding 
  63.         if(this.status === "pending"){ 
  64.             this.onResolvedCallbacks.push(function(){ 
  65.                 try{ 
  66.                     resolve(onFulfilled(this.value)) 
  67.                 }catch(error){ 
  68.                     reject(error) 
  69.                 } 
  70.             }); 
  71.             this.onRejectedCallbacks.push(function(){ 
  72.                 try { 
  73.                     resolve(onRejected(this.reason)) 
  74.                 } catch (error) { 
  75.                     reject(error) 
  76.                 } 
  77.             }); 
  78.         } 
  79.     }) 

升级版Promise:

  1. class Promise{ 
  2.     /*首先定义一个构造函数,在创建Promise对象的时候会传递一个函数executor, 
  3.     这个函数会立即被调用,所以我们在Promise内部立即执行这个函数。*/ 
  4.     constructor(executor){ 
  5.         this.executor = executor(this.resolve,this.reject); 
  6.         
  7.  
  8.         this.onResolvedCallbacks = [];//存放所有成功的回调函数 
  9.         this.onRejectedCallbakcs = [];//存放所有失败的回调函数 
  10.     } 
  11.      
  12.      // 用于存储相应的状态 
  13.     status = "pending"
  14.     // 初始值 
  15.     value; 
  16.     // 初始原因 
  17.     reason; 
  18.      
  19.     // executor在执行的时候会传入两个方法,一个是resolve, 
  20.     // 一个reject,所以我们要创建这两个函数,而且需要把这两个函数传递给executor。 
  21.     // 当我们成功或者失败的时候,执行onFulfilled和onRejected的函数, 
  22.     // 也就是在resolve函数中和reject函数中分别循环执行对应的数组中的函数。 
  23.     // 定义成功事件 
  24.     resolve(value){ 
  25.         if(status === "pending"){ 
  26.             status = "resolved"
  27.             value = value; 
  28.             this.onResolvedCallbacks.forEach(fn=>{fn()}) 
  29.         } 
  30.     } 
  31.     // 定义失败事件 
  32.     reject(){ 
  33.         if(this.status === "pending"){ 
  34.             this.status = "rejected"
  35.             this.reason = reason; 
  36.             this.onRejectedCallbakcs.forEach(fn=>{fn()}); 
  37.         } 
  38.     } 
  39.     // 这个时候当我们异步执行resolve方法时候,then中绑定的函数就会执行,并且绑定多个then的时候,多个方法都会执行。 
  40.  
  41.  
  42.     // Promise的对象存在一个then方法,这个then方法里面会有两个参数,一个是成功的回调onFulfilled, 
  43.     // 另一个是失败的回调onRejected,只要我们调用了resolve就会执行onFulfilled,调用了reject就会执行onRejected。 
  44.     // 为了保证this不错乱,我们定义一个self存储this。当我们调用了resolve或reject的时候,需要让状态发生改变. 
  45.     // 需要注意的是Promise的状态只可改变一次,所以我们要判断,只有当状态未发生改变时,才去改变状态。 
  46.     then(onFulfilled,onRejected){ 
  47.         // 判断当前状态进行回调 
  48.         if(this.status === "resolved"){ 
  49.             onFulfilled(self.value) 
  50.         }; 
  51.         if(this.status === "rejected"){ 
  52.             onRejected(self.reason) 
  53.         } 
  54.         // 当状态还处于pending状态时 
  55.         // 因为onFulfilled和onRejected在执行的时候需要传入对应的value值,所我们这里用一个函数包裹起来,将对应的值也传入进去。 
  56.         if(this.status === "pending"){ 
  57.             this.onResolvedCallbacks.push(()=>{onFulfilled(this.value)}); 
  58.             this.onResolvedCallbacks.push(()=>{onRejected(this.reason)}); 
  59.         } 
  60.     } 
  61.  

使用自己手写的Promise源码:

  1. let p = new Promise((resolve,reject)=>{ 
  2.     setTimeout(()=>{ 
  3.         resolve("成功了"
  4.     },1000) 
  5. }); 
  6.  
  7. p.then(function(value){ 
  8.     return 123; 
  9. }).then(value=>{ 
  10.     console.log("收到了成功的消息:",value); 
  11. }).catch(error=>{ 
  12.     console.log(error); 
  13. }); 
  14.  
  15. p.then(value=>{ 
  16.     console.log(value); 
  17. }) 

四、Async/Await

async用来表示函数是异步的,定义的async函数返回值是一个promise对象,可以使用then方法添加回调函数。

await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。函数中只要使用await,则当前函数必须使用async修饰。

所以回调函数的终结者就是async/await。

async命令

  • async函数返回的是一个promise对象。
  • async函数内部return语句返回的值,会成为then方法回调的参数。
  • async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。
  • 抛出的错误对象会被catch方法回调函数接收到。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。

也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

  1. async function fun(){ 
  2.     // return "hello wenbo"
  3.     throw new Error("ERROR"); 
  4. fun().then(v => console.log(v),reason=>console.log(reason));//Error: ERROR``` 

await命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

  1. async function fun(){ 
  2.     return await "zhaoshun"
  3.     // 等价于 return "zhaoshun"
  4. fun().then(value=>console.log(value));//zhaoshun 

另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。

  1. class Sleep{ 
  2.     constructor(timeout){ 
  3.         this.timeout = timeout; 
  4.     } 
  5.     then(resolve,reject){ 
  6.         const startTime = Date.now(); 
  7.         setTimeout(()=>resolve(Date.now() - startTime),this.timeout); 
  8.          
  9.  
  10.     } 
  11.  
  12.  
  13.     async ()=>{ 
  14.         const sleepTime = await new Sleep(1000); 
  15.         console.log(sleepTime);//1012 
  16.     } 
  17. )() 
  18.  
  19.  
  20. // js里面没有休眠的语法,但是借助await命令可以让程序停顿的时间 
  21. const sleepFun = (interval) => { 
  22.     return new Promise(resolve=>{ 
  23.         setTimeout(resolve,interval); 
  24.     }) 
  25.  
  26. // 用法 
  27. const asyncFun = async ()=>{ 
  28.     for(let i = 1; i <= 5; i++){ 
  29.         console.log(i); 
  30.         await sleepFun(1000); 
  31.     } 
  32. asyncFun(); 

从上面可以看到,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

注意:上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

  • 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
  • 使用try/catch可以很好处理前面await中断,而后面不执行的情况。

示例:

  1. const fun = async ()=>{ 
  2.     try { 
  3.         await Promise.reject("ERROR"); 
  4.     } catch (error) { 
  5.          
  6.  
  7.     } 
  8.     return await Promise.resolve("success"); 
  9.  
  10. fun().then
  11.     value=>console.log(value),reason=>console.log(reason,"error")// 
  12. ).catch( 
  13.     error=>console.log(error)//ERROR 
  14. ); 

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

  1. const fun = async ()=>{ 
  2.     await Promise.reject("error").catch(e=>console.log(e)); 
  3.     return await Promise.resolve("success"); 
  4. fun().then(v=>console.log(v));//success 

错误处理

第一点:如果await后面的异步操作出错,那么等同于async函数后面的promise对象被reject。

  1. const fun = async()=>{ 
  2.     await new Promise((resolve,reject)=>{ 
  3.         throw new Error("error"
  4.     }) 
  5. fun().then(v=>console.log(v)).catch(e=>console.log(e)); 

第二点:多个await命令后面的异步操作,如果不存在继发关系,最好让他们同时进行触发。

  1. const [fun1,fun2] = await Promise.all([getFun(),getFoo()]); 
  2.  
  3. const fooPromise = getFoo(); 
  4. const funPromise = getFun(); 
  5. const fun1 = await fooPromise(); 
  6. const fun2 = await funPromise(); 

第三点:await命令只能用在async函数之中,如果用在普通函数,就会报错。

  1. async function dbFuc(db) { 
  2.   let docs = [{}, {}, {}]; 
  3.  
  4.   // 报错 
  5.   docs.forEach(function (doc) { 
  6.     await db.post(doc); 
  7.   }); 

第四点:async 函数可以保留运行堆栈。

小结在这篇文章中我们总结了异步编程和回调函数的解决方案Promise,以及回调终结者aysnc/await。

  • Promise有三个状态:pending、rejected、resolved。
  • Promise的状态改变,只能改变一次,只有两种改变:pending变为resolved/fullfilled、pending变为rejected。
  • Promise最大优点就是可以进行链式调用。
  • async用来表示函数是异步的,定义的async函数返回值是一个promise对象。
  • 函数中只要使用await,则当前函数必须使用async修饰。
  • 回调函数的终结者就是async/await。

参考文章

  • 《异步终结者 async await,了解一下》
  • 《一次性让你懂async/await,解决回调地狱》
  • 《面试精选之Promise》
  • 《BAT前端经典面试问题:史上最最最详细的手写Promise教程》
  • 《Promise实现原理(附源码)》
  • 《JavaScript高级程序设计(第四版)》
  • 《你不知道的JavaScript(中卷)》
  • 阮一峰《ES6 入门教程》

本文转载自微信公众号「前端万有引力」,可以通过以下二维码关注。转载本文请联系前端万有引力公众号。

 

责任编辑:姜华 来源: 前端万有引力
相关推荐

2023-10-08 10:21:11

JavaScriptAsync

2021-06-07 09:44:10

JavaScript开发代码

2017-04-10 15:57:10

AsyncAwaitPromise

2024-09-02 14:12:56

2023-03-29 10:19:44

异步编程AsyncPromise

2017-06-19 09:12:08

JavaScriptPromiseAsync

2022-11-21 09:01:00

Swift并发结构

2014-07-15 10:08:42

异步编程In .NET

2021-06-09 07:01:30

前端CallApply

2014-07-15 10:31:07

asyncawait

2024-02-01 09:39:02

asyncawaitPromise

2016-11-22 11:08:34

asyncjavascript

2021-07-15 14:29:06

LRU算法

2024-03-05 18:15:28

AsyncAwait前端

2021-09-06 08:13:35

APM系统监控

2022-06-24 08:33:13

ECMAScriptjavaScript

2012-07-22 15:59:42

Silverlight

2021-07-20 10:26:12

JavaScriptasyncawait

2022-08-27 13:49:36

ES7promiseresolve

2023-07-28 07:31:52

JavaScriptasyncawait
点赞
收藏

51CTO技术栈公众号