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

敢说敢作敢为, 无怨无恨无悔。这篇文章主要讲述前端开发:解决异步回调必备技能——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 =https://www.songbingjia.com/android/> console.log(data));


function f2()
return new Promise((resolve, reject) =>
setTimeout(() => resolve(22), 2000);
).then(data =https://www.songbingjia.com/android/> console.log(data)); ;


function f3()
return new Promise((resolve, reject) =>
setTimeout(() => resolve(33), 3000);
).then(data =https://www.songbingjia.com/android/> 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构造函数里面有两个参数:resolve和reject,该两个参数表示的是异步操作的结果,也就是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有两个参数: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=https://www.songbingjia.com/android/>

console.log(value)//这里的value就是上面的yes

,reason=>

console.error(reason)

)

2.4.2 catch?
catch指定失败的回调,返回的是失败的结果。实例如下所示:
?
let p =new Promise((resolve,reject)=>

reject(失败!’);
)
p.then(value=https://www.songbingjia.com/android/> ,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); //p2ok

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的。
?
③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基本含义?
①Async/Await是基于Promise实现的,是写异步代码的新方式,它们不能用于普通的回调函数;
?
②Async/Await也是非阻塞的;
?
③Async/Await的写法使得异步代码看起来像同步代码,简洁、便于读取。
?
3.1.2Async/Await的语法?
async必须声明的是一个function函数,await就必须是在async声明的函数内部使用,这是一个固定搭配,任何一种不符合这两个条件,程序就会报错,具体举实例来直观看一下:
?
let data = https://www.songbingjia.com/android/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 = https://www.songbingjia.com/android/JSON.parse(await getJSON())

console.log(data)

catch (err)

console.log(err)




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

const data = https://www.songbingjia.com/android/await getJSON()

if (data.anotherRequest)

const moreData = https://www.songbingjia.com/android/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一个值(“”、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是前提。
?
?
?
【前端开发(解决异步回调必备技能——Async/Await和Promise)】


    推荐阅读