写点什么

前端开发解决异步回调必备技能——Async/Await 和 Promise

用户头像
三掌柜
关注
发布于: 2021 年 04 月 10 日
前端开发解决异步回调必备技能——Async/Await和Promise

做过前端开发的开发者应该都知道 JavaScript 是单线程语言,浏览器只分配给 JS 一个主线程,用来执行任务,但是每次一次只能执行一个任务,这些任务形成一个任务队列排队等候执行;但是某些任务是比较耗时的,如网络请求,事件的监听,以及定时器,如果让这些非常耗时的任务一一排队等候执行,那么程序执行效率会非常的低,甚至会导致页面假死。因此,浏览器为这些耗时的任务开辟了新的线程,主要包括 http 请求线程、浏览器事件触发线程、浏览器定时触发器,但是这些任务都是异步的,这就涉及到了前端开发的异步回调操作处理。


本文开始先通过介绍 Promise 和 Async/Await 的介绍和详细用法,然后再根据实战中的注意事项,方便大家理解和使用。

一、Async/Await 出现的原因

在 ES7 之前,了解到 Promise 是 ES6 为了解决异步回调而产生的解决方案,避免出现回调地狱(Callback Hell),那么 ES7 为什么又提出了新的 Async/Await 标准?


问题答案就是:Promise 虽然解决了异步嵌套的怪圈,使用表达清晰的链式表达;但是如果在实际开发过程中有些地方有大量的异步请求的时候,而且流程复杂嵌套的情况下,检查相关代码会发现一个比较尴尬的情景,到处都是 then,查阅和修改起来比较费神费力,因此 ES7 提出新的 Async/Await 标准就是为了解决这种尴尬情形的。


但是在介绍 Async/Await 之前,首先来详细深入的了解一下 Promise 的相关内容。

二、Promise 相关的内容

2.1 Promise 诞生的背景

是 ES6 里面的新技术,为的是解决异步回调地狱问题。

2.1.1 回调地狱(Callback Hell)

也叫回调嵌套或者函数混乱的调用,通俗点讲就是:需要发送三个网络请求,第三个请求依赖第二个请求的结果,第二个请求依赖第一个请求的结果。不断增加的嵌套使用。

2.1.2 回调函数的弊病

开发者阅读起来很费神、吃力,不利于排查错误,更不能直接 return,等等。如:

setTimeout(() => {   console.log(1)    setTimeout(() => {      console.log(2)        setTimeout(() => {           console.log(3)           },3000)             },2000)},1000)
复制代码

2.1.3 常用回调函数

使用场景有:网络请求、事件的监听、定时器等,常见回调函数有:AJAX、计时器、fs 等。

2.1.4 Promise 解决异步回调地狱方法

针对上述回调函数使用遇到的弊端,Promise 就是为解决此问题而生的,如下所示:

function f1() {   return new Promise((resolve, reject) => {      setTimeout(() => resolve(11), 1000);   }).then(data => console.log(data));}function f2() {   return new Promise((resolve, reject) => {      setTimeout(() => resolve(22), 2000);   }).then(data => console.log(data));;}function f3() {   return new Promise((resolve, reject) => {      setTimeout(() => resolve(33), 3000);   }).then(data => console.log(data));;}f1().then(f2).then(f3)
复制代码

2.2 什么是 Promise

Promise 是什么?不知道作为前端开发的你能否正确的说出 Promise 的学术概念,如果说不出来也不要紧,直接看下面解释。

2.2.1 Promise 对象

Promise 是一个对象,从其中可以获取异步操作的消息,可以说它更像是一个容器,保存着将来才会结束的事件(也就是一个异步操作)。


Promise 对象其实表示是一个异步操作的最终成败,以及结果值,也就是一个代理值,是 ES6 中的一种异步回调解决方案。

2.2.2 Promise 的语法定义

Promise 是一个构造函数,用来生成 Promise 的实例对象。

2.2.3 Promise 的功能定义

Promise 对象用来包裹一个异步操作,并且获取操作成功和失败的结果值。

2.3 Promise 的三种状态

Promise 对象代理的值其实是未知的,状态是动态可变的,因此 Promise 对象的状态有三种:进行中、结束、失败,它运行的时候,只能从进行中到失败,或者是从进行中到成功。使用 Promise 对象只要是通过同步的表达形式来运行异步代码。

  1. pending:初始状态,既不成功,也不失败;

  2. fulfilled:操作成功结束;

  3. rejected:操作失败。


2.3.1 Promise 的基本使用

Promise 构造函数里面有两个参数:resolve 和 reject,该两个参数表示的是异步操作的结果,也就是 Promise 成功或失败的状态。


  1. 异步操作成功,调用 resolve 函数,将 Promise 对象的状态改为 fulfilled。

  2. 异步操作失败,调用 rejected 函数,将 Promise 对象的状态改为 rejected。

举一个使用例子,比较规范的写法是把 Promise 封装到一个函数里然后同时返回一个 Promise,如下所示:

const delay = (millisecond) => {  return new Promise((resolve, reject)=>{      if (typeof millisecond != 'number') reject(new Error(‘必须是number类型'));      setTimeout(()=> {        resolve(`延迟${millisecond}毫秒后输出`)      }, millisecond)  })}
复制代码

上述例子可以看到 Promise 有两个参数:resolve 和 reject。resolve:将异步的执行从 pending(请求)变成了 resolve(成功返回),是个函数执行返回;reject:见名知意为“拒绝”,从请求变成了"失败",是函数可以执行返回的一个失败结果,推荐返回一个错误 new Error(),这样做更加清晰明了,更加规范。

2.3.2 resolve 函数

若传入的是非 Promise,基本数据类型的数据,则返回成功的 Promise;若传入的是 Promise,则该对象的结果就决定了 resolve 的返回结果值。

let obj =new Promise((resolve,reject)=>{  resolve(‘yes’);});
//1.若传入的是非Promise,基本数据类型的数据,则返回成功的Promise。let p1= Promise.resolve(’123’)
//2.若传入的是Promise,则该对象的结果就决定了resolve的返回结果值。let p2 = Promise.resolve(obj);
//3.嵌套使用let p3 = Promise.resolve(Promise.resolve(Promise.resolve('abc')));
console.log(p3);
复制代码

2.3.3 rejected 函数

Promise.prototype.reject,始终返回的是失败的 Promise

let p = Promise.reject(123123);let p2 = Promise.reject('abc');let p3 = Promise.reject(Promise.resolve('ok'));console.log(p3);
复制代码

2.4 Promise 的 API

Promise 的 API 里面常用的几个方法有:then、catch、finally、all、race 等,具体的使用方法下面一一道来。

2.4.1 then

then 指定成功或失败的回调到当前的 Promise。then 里面拿到的 Promise resolve 里面的数据,并返回一个 Promise 继续提供使用;then 方法返回的结果由 then 指定回调函数决定。实例如下所示:

let p=new Promise((resolve,reject)=>{  resolve(‘yes’)})p.then(value=>{  console.log(value)  //这里的value就是上面的yes},reason=>{  console.error(reason)  })
复制代码

2.4.2 catch

catch 指定失败的回调,返回的是失败的结果。实例如下所示:

let p =new Promise((resolve,reject)=>{  reject('失败!’);})p.then(value=>{},reason=>{  console.error(reason);})p.catch(reason=>{  console.error(reason)})
复制代码

2.4.3 finally

finally 用来进行收尾工作,无论 Promise 的状态成功和失败与否,当执行完回调函数之后,都会去 finally 寻找最后的回调函数来执行。实例如下所示:

request.finally(function(){    // 最后,而且一定会执行的代码})
复制代码

2.4.4 Promise.all

在多个 Promise 任务一起执行的时候,若全部成功,则返回一个新的 Promise,若其中有一个失败,则返回失败的 Promise 对象。实例如下所示:

let p1 = new Promise((resolve, reject) => {  setTimeout(() => {    resolve(‘yes’);  }, 1000);});let p2 = Promise.resolve('ok');let p3 = Promise.reject('Oh no’);
//所有的let result = Promise.all([p1, p2, p3]);console.log(result);
复制代码

2.4.5 Promise.race

在多个 Promise 任务同步执行的时候,返回最先结束的 Promise 任务结果,无论最后是成功还是失败,通俗点将:先到先得。实例如下所示:

let p1 = new Promise((resolve, reject) => {  setTimeout(() => {    resolve(‘yes’);  }, 1000);});let p2 = new Promise((resolve, reject) => {  setTimeout(() => {    resolve('ok');  }, 500);});let result = Promise.race([p1, p2]);console.log(result);   //p2  ok
复制代码

2.5 Promise 的链式调用

2.5.1 链式调用的使用场景

常见的连续执行两个及以上的异步操作,每一个后来的操作都是建立在前面操作执行成功之后,并且带着上一步操作所返回的结果开始执行的,通过构造一个 Promise chain 来完成这种需求的操作过程叫做链式调用。实例如下所示:

let p = new Promise((resolve, reject) => {    setTimeout(resolve, 1000, 'success');});p.then(   res => {     console.log(res);     return new Promise((resolve, reject) => {        setTimeout(resolve, 1000, 'success');     });   }) .then(    res => console.log(res) );  // 相隔1000ms —> success —> success
复制代码

JS 的执行顺序:同步—>异步—>回调,在执行同步的时候,Promise 对象还处于 pending 的状态,也就是说明 then 返回的是一个 Promise 对象,而且必须在 then 里面给一个返回值,这样才能继续调用,否则就会 undefined。

2.5.2 链式写法注意事项

  1. then 式——链式写法的本质其实就是一直往下传递返回一个新的 Promise,也就是 then 在下一步接收的是上一步返回的 Promise

  2. catch 写法是针整个链式写法的错误捕获,而 then 第二个参数是针对于上一个返回 Promise 的。

  3. catch 和 then 的优先级:看谁在链式写法的前面,在前面的先捕获到错误,后面的就没有错误可捕获了,链式前面的优先级大,而且两者都不是 break,可以继续执行后续操作不受影响。

2.5.3 链式写法的错误处理

在链式调用的时候,Promise 有很多,那么 catch 要不要也写同样多?答案是否定的,因为链式写法的错误处理具有“冒泡”特性,在链式中任何一个环节出现问题,都能被 catch 到,与此同时在某个环节后面的代码就不会执行,所以只需在末尾 catch 一下就可以了。


如果把 catch 移到第一个链式返回里面,链式会继续往下走,说明链式中的 catch 不是最终的终点,catch 只是一个捕获错误的链式表达并不是 break。

2.5.4 链式返回自定义值

Promise 链式返回自定义值,直接用 Promise 原型方法 resolve 来操作即可,实例如下所示:

delay(1000).then((result)=>{  console.log('第一步完成');  console.log(result);  let message = '这是想要处理的值';   return Promise.resolve(message) // 返回在下一阶段处理的值}).then((result)=>{  console.log('第二步完成');  console.log(result); // 拿到上一阶段的返回值  //return Promise.resolve('这里可以继续返回')}).catch((err)=>{  console.log(err);})
复制代码

2.5.5 停止或者跳出 Promise 链式

不管是因为错误跳出还是主动跳出 Promise 链式,都需要加一个标志位,如下所示:

return Promise.reject({    isNotErrorExpection: true // 在返回这里加一个标志位,判断是否是错误类型,如果不是那么说明可以是主动跳出循环}) 
复制代码

三、Async/Await 相关的内容

3.1 Async/Await 是什么?

Async/Await 是基于 Promise 而来的,Async/Await 是相互依存的,缺一不可,它们的出现就是为了 Promise 而来,也算是 Promise 的进化改良版,为的就是解决文章开始说的如果出现大量复杂嵌套不易读的 Promise 异步问题。

3.1.1Async/Await 基本含义

  1. Async/Await 是基于 Promise 实现的,是写异步代码的新方式,它们不能用于普通的回调函数;

  2. Async/Await 也是非阻塞的;

  3. Async/Await 的写法使得异步代码看起来像同步代码,简洁、便于读取。

3.1.2Async/Await 的语法

async 必须声明的是一个 function 函数,await 就必须是在 async 声明的函数内部使用,这是一个固定搭配,任何一种不符合这两个条件,程序就会报错,具体举实例来直观看一下:

let data = 'data'a = async function () {  const b = function () {     await data  }}
复制代码

3.2 Async/Awaitd 的本质

3.2.1 Async 的本质

async 是对 generator 的再一次语法糖封装,帮助实现了生成器的调用,使语句更贴近同步代码的表达方式,可以将 async 函数看做是多个异步操作封装的 promise 对象。


async 声明的函数的返回其实就是一个 Promise,也就是说只要声明的函数是 async,不管内部怎么处理,它返回的一定是一个 Promise,举个例子如下所示:

(async function () {    return 'Promis+++‘})() // 返回的是Promise+++
复制代码

3.2.2 Awaitd 的本质

await 的本质其实是可以提供等同于“同步效果”的等待异步返回能力的语法糖,也就是 then 的语法糖。如果想使用 await 来执行一个异步操作,那么其调用函数必须使用 async 来声明。


await 能返回一个 Promise 对象,也能返回一个值。若 await 返回的是 Promise 对象,那么还可以继续给 await 的返回值使用 then 函数。举个实例看一下:

const a = async ()=>{    let message = '声明值111’    let result = await message;    console.log('由于上面的程序还没执行完,“等待一会”');    return result}a().then(result=>{  console.log('输出',result);})
复制代码

3.3 Async/Await 的优势

为什么说 Async/Awaitd 比 Promise 更胜一筹?具体原因如下所示。

3.3.1 简洁明了

根据上述关于 Async/Awaitd 的实例可以看到,Async/Awaitd 的写法很简单,相比 Promise 的写法,不用写.then,不用写匿名函数处理 Promise resolve 值,也不用定义多余的 data 变量,更避免了嵌套代码的操作,大大省去了很多代码行,使得处理异步操作的代码简洁明了,方便查阅和精准定位。

3.3.2 错误处理的方式

Async/Await 可以让 try/catch 同时处理同步和异步的错误,而且在 Promise 中 try/catch 不能处理 JSON.parse 的错误,在 Promise 中需要使用.catch,但是错误处理的那坨代码会非常冗余,要知道实际开发过程中代码会比理论上的情况会更复杂。


通过使用 Async/Await,try/catch 能处理 JSON.parse 的错误,具体实例如下所示:

const request = async () => {   try{         const data = JSON.parse(await getJSON())         console.log(data)      } catch (err) {          console.log(err)      } }
复制代码

3.3.3 条件语句

通过使用 Async/Await,可以使得条件语句写法简洁又可以提高代码可读性,这里就不再举对比的例子,只举一个 Async/Await 的使用实例来说:

const request = async () => {  const data = await getJSON()  if (data.anotherRequest) {    const moreData = await anotherRequest(data);    console.log(moreData)    return moreData  } else {    console.log(data)    return data      }}
复制代码

3.3.4 中间值

在实际开发过程中会遇到这种场景:调用 promise1,使用 promise1 返回的结果再去调用 promise2,然后使用两者的结果去调用 promise3。在没有使用 Async/Await 之前的写法,应该是这样的:

const request = () => {  return promise1()    .then(value1 => {      return promise2(value1)        .then(value2 => {                  return promise3(value1, value2)        })    })}
复制代码

使用了 Async/Await 的写法之后,是这样的:

const request = async () => {  const value1 = await promise1()  const value2 = await promise2(value1)  return promise3(value1, value2)}
复制代码

通过上述两个写法,直观的看出来使用 Async/Await 之后会使得代码变得非常整洁简单,直观,高可读性。

3.3.5 错误栈对比

如果实例中调用多个 Promise,其中的一个 Promise 出现错误,Promise 链中返回的错误栈没有显示具体的错误发生的位置信息,这就造成排查错误的耗时时长和解决的难度,甚至会起到反作用,假设错误栈的唯一函数名为 errorPromise,但是它和错误没有直接关系。如果使用了 Async/Await 之后,错误栈会直接指向错误所在的函数,这样更清晰直观的方便排查问题所在,尤其是在查看分析错误日志的时候非常有效有用。

3.3.6 调试

通过上面描述的 Async/Await 优点中,一直在反复强调 Async/Await 会使得代码简洁明了,其实在调试过程中,Async/Await 也可以使得代码调试起来很轻松简单,相对于 Promise 来讲,不用再写太多箭头函数,可以直接像调试同步代码一样单步走,跳过 await 语句。

3.3.7 中断/终止程序

首先要明确知道,Promise 自身是不能终止的,Promise 本身只是一个状态机,存储了三种状态,一旦进行发出请求,就必须要闭环,无法进行取消操作,就算是在前面讲到的 pending 状态,也只是一个挂起请求的状态,但不是取消。


但是使用 Async/Await 的时候,想要终止程序就很简单,那是因为 Async/Await 语义化很明显,和一般的 function 的写法类似,想要终端程序的时候,直接 return 一个值(“”、null、false 等)就可以了,实际上就是直接返回一个 Promise。具体实例如下所示:

let count = 3;const a = async ()=>{  const result = await delay(2000);  const result1 = await delaySecond(count);  if (count > 2) {      return '';    // return false;     // return null;  }  console.log(await delay(2000));  console.log(‘结束’);};a().then(result=>{  console.log(result);}).catch(err=>{  console.log(err);})
复制代码

async 函数本质就是返回一个 Promise。

四、实际开发过程中异步操作需要注意的事项

在前端实际开发过程中,只要涉及到异步操作的时候就必定会用到 Promise 或者 Async/Await。前面文章详细的分析了二者的使用以及对比,也不能单纯的来说只能用 Async/Await 而不用 Promise,到目前为止,这两种解决异步回调的方式都是很有效的,而且这二者还可以混合使用,主要是根据具体的实际开发需求场景来决定。

4.1 Promise 使用 then 获取数据(串行)

在实际开发过程中,如果需要串行循环一个请求操作,就需要依次加延迟来输出值,如果稍有不慎就会出错,所以需要更更加小心留意写法。解决串行的情况执行的还是并行的方法,就是通过直接函数名存储函数的方式,相当于把 Promise 预先存储在一个数组中,在需要调用的时候再去执行,完美解决。

4.2 Promise 通过 for 循环获取数据(串行)

关于 Promise 通过 for 循环获取数据,首先来分享一个高端的写法,通过 reduce 迭代数组的过程的写法:

array = [timeout(2000), timeout(1000), timeout(1000)]const a = array.reduce((total, current)=>{    return total.then((result)=>{        console.log(result);        return current()    })}, Promise.resolve('开始'))a.then((result)=>{    console.log('结束', result);})
复制代码

再分享一种常规写法,这样容易理解一点,具体如下所示:

array = [timeout(2000), timeout(1000), timeout(1000)]	const syncPromise = function (array) {  	const _syncLoop = function (count) {   		if (count === array.length - 1) { // 是最后一个直接return      	 return array[count]()    	}    	return array[count]().then((result)=>{      		console.log(result);      		return _syncLoop(count+1) // 递归调用数组下标    	});  	}  	return _syncLoop(0);	}	syncPromise(array).then(result=>{  		console.log('完成');	})	// 添加在Promise类中方法	Promise.syncAll = function syncAll(){  		return syncPromise	}	Promise.syncAll(array).then(result=>{  		console.log(result);  		console.log('完成');	})
复制代码

4.3 Async/Await 使用 for 循环获取数据(串行)

根据上面 Promise 的 for 循环获取数据来做对比,直接使用上述实例的场景,来看看 Async/Await 的写法,具体操作如下所示:

(async ()=>{  array = [timeout(2000), timeout(1000), timeout(1000)]    for (var i=0; i < array.length; i++) {      result = await array[i]();        	console.log(result);    	}	})()
复制代码

通过对比,在这里还要夸一下 Async/Await,直观的可以看到同样的需求,使用 Async/Await 来实现是不是非常的方便和简洁。

五、总结

通过上面 Async/Await 和 Promise 的对比介绍,可以知道 Async/Await 是近年来 JS 新增的最具革命性的特性之一,Async/Await 会让你看到 Promise 的语法有多糟糕,而且提供了一个直观的替代方法。


但是对于 Async/Await 你肯定也许会有一些怀疑和顾虑,因为 Node7 不是 LTS(长期支持版本),但是代码迁移很简单,不必担心版本是否稳定的问题。还有就是大部分开发者已经习惯了使用回调函数或者.then 来识别异步代码,Async/Await 使得异步代码不在“明显”(因为 Async/Await 使得代码看起来像同步代码),但是在了解使用之后,会很快消除这种短暂的不适应。


其实上述两点只是分析一下未来的趋势,但是短期内 Promise 肯定不会因为 Async/Await 的出现而立马淘汰出局,也可以说正是有了 Promise 才有了升级改良版的 Async/Await,二者是相互依存,缺一不可的,要想学好前端开发的 Async/Await,学习好 Promise 是前提。

最后

以上是本篇文章所有内容,也参考了网络上优秀的知识点,由于本人水平有限,如有问题请指正,前端菜鸟欢迎各路大佬的批评和建议。

发布于: 2021 年 04 月 10 日阅读数: 224
用户头像

三掌柜

关注

某某某技术有限责任公司架构师 2021.02.05 加入

一分耕耘,不一定有一分收获,但十分耕耘,一定会有一分收获!

评论 (31 条评论)

发布
用户头像
很实用,收藏了 加油加油💪
2021 年 05 月 07 日 23:21
回复
用户头像
膜拜,厉害👍
2021 年 04 月 24 日 12:11
回复
用户头像
Mark~
2021 年 04 月 12 日 14:36
回复
用户头像
赞👍
2021 年 04 月 11 日 14:33
回复
用户头像
谢谢,有帮助
2021 年 04 月 11 日 13:32
回复
用户头像
牛哇牛哇,大佬可以教我嘛
2021 年 04 月 11 日 12:17
回复
没有问题😁
2021 年 04 月 11 日 12:25
回复
用户头像
优秀
2021 年 04 月 11 日 12:16
回复
谢谢支持😁
2021 年 04 月 11 日 12:24
回复
用户头像
专业大佬
2021 年 04 月 11 日 11:13
回复
😁
2021 年 04 月 11 日 11:52
回复
用户头像
厉害
2021 年 04 月 11 日 08:19
回复
😁
2021 年 04 月 11 日 11:52
回复
用户头像
牛啤~
2021 年 04 月 11 日 01:19
回复
欢迎大佬来捧场
2021 年 04 月 11 日 11:52
回复
用户头像
已收藏,膜拜大佬
2021 年 04 月 11 日 00:26
回复
多谢支持
2021 年 04 月 11 日 11:52
回复
用户头像
很可,排版看着很舒服,结构清晰完整,赞
2021 年 04 月 11 日 00:17
回复
😁
2021 年 04 月 11 日 11:52
回复
用户头像
大神牛逼!👍
2021 年 04 月 11 日 00:17
回复
哈哈
2021 年 04 月 11 日 11:51
回复
用户头像
加油,你肯定是前100名以内的。
2021 年 04 月 11 日 00:05
回复
😁
2021 年 04 月 11 日 11:51
回复
用户头像
赞赞赞
2021 年 04 月 11 日 00:04
回复
😁
2021 年 04 月 11 日 11:51
回复
用户头像
这质量,我感觉有了!
2021 年 04 月 10 日 19:09
回复
哈哈,多谢支持
2021 年 04 月 10 日 23:59
回复
用户头像
用心了,赞👍🏻
2021 年 04 月 10 日 18:51
回复
多谢
2021 年 04 月 10 日 23:59
回复
用户头像
Mark
2021 年 04 月 10 日 18:38
回复
谢谢支持,坐等官方翻牌
2021 年 04 月 10 日 18:48
回复
没有更多了
前端开发解决异步回调必备技能——Async/Await和Promise