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

Posted 三掌柜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端开发:解决异步回调必备技能——Async/Await和Promise相关的知识,希望对你有一定的参考价值。

前端开发:解决异步回调必备技能——Async/Await和Promise_异步操作

前言

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

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

一、Async/Await出现的原因

ES7之前,了解到PromiseES6为了解决异步回调而产生的解决方案,避免出现回调地狱(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对象只要是通过同步的表达形式来运行异步代码。

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

②fulfilled:操作成功结束;

③rejected:操作失败。

2.3.1 Promise的基本使用

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

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

异步操作失败,调用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有两个参数:resolverejectresolve:将异步的执行从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

PromiseAPI里面常用的几个方法有:thencatchfinallyallrace等,具体的使用方法下面一一道来。

2.4.1 then

then指定成功或失败的回调到当前的Promisethen里面拿到的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链式写法注意事项

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

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

③catchthen的优先级:看谁在链式写法的前面,在前面的先捕获到错误,后面的就没有错误可捕获了,链式前面的优先级大,而且两者都不是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基本含义

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

②Async/Await也是非阻塞的;

③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/AwaitdPromise更胜一筹?具体原因如下所示。

3.3.1 简洁明了

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

3.3.2 错误处理的方式

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

通过使用Async/Awaittry/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 = () => 

eturn 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一个值(“”nullfalse)就可以了,实际上就是直接返回一个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循环获取数据(串行)

根据上面Promisefor循环获取数据来做对比,直接使用上述实例的场景,来看看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/AwaitPromise的对比介绍,可以知道Async/Await是近年来JS新增的最具革命性的特性之一,Async/Await会让你看到Promise的语法有多糟糕,而且提供了一个直观的替代方法。

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

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


以上是关于前端开发:解决异步回调必备技能——Async/Await和Promise的主要内容,如果未能解决你的问题,请参考以下文章

解决异步问题

前端必备 HTTP 技能之 Ajax 技术详解

前端开发必备的 5 个「高薪」技能

前端必备技能~H5网页无法转发给朋友和分享朋友圈,解决方法汇总

解决vue跨域axios异步通信

想学Web前端,你需要了解这些职责和必备技能