赞
踩
本文开始先通过介绍 Promise 和 Async/Await 的介绍和详细用法,然后再根据实战中的注意事项,方便大家理解和使用。
在 ES7 之前,了解到 Promise 是 ES6 为了解决异步回调而产生的解决方案,避免出现回调地狱(Callback Hell),那么 ES7 为什么又提出了新的 Async/Await 标准?
问题答案就是:Promise 虽然解决了异步嵌套的怪圈,使用表达清晰的链式表达;但是如果在实际开发过程中有些地方有大量的异步请求的时候,而且流程复杂嵌套的情况下,检查相关代码会发现一个比较尴尬的情景,到处都是 then,查阅和修改起来比较费神费力,因此 ES7 提出新的 Async/Await 标准就是为了解决这种尴尬情形的。
但是在介绍 Async/Await 之前,首先来详细深入的了解一下 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)
复制代码
Promise 是什么?不知道作为前端开发的你能否正确的说出 Promise 的学术概念,如果说不出来也不要紧,直接看下面解释。
2.2.1 Promise 对象
Promise 是一个对象,从其中可以获取异步操作的消息,可以说它更像是一个容器,保存着将来才会结束的事件(也就是一个异步操作)。
Promise 对象其实表示是一个异步操作的最终成败,以及结果值,也就是一个代理值,是 ES6 中的一种异步回调解决方案。
2.2.2 Promise 的语法定义
Promise 是一个构造函数,用来生成 Promise 的实例对象。
2.2.3 Promise 的功能定义
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);
复制代码
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.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 是基于 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.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);
})
复制代码
为什么说 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,到目前为止,这两种解决异步回调的方式都是很有效的,而且这二者还可以混合使用,主要是根据具体的实际开发需求场景来决定。
在实际开发过程中,如果需要串行循环一个请求操作,就需要依次加延迟来输出值,如果稍有不慎就会出错,所以需要更更加小心留意写法。解决串行的情况执行的还是并行的方法,就是通过直接函数名存储函数的方式,相当于把 Promise 预先存储在一个数组中,在需要调用的时候再去执行,完美解决。
关于 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('完成');
})
复制代码
根据上面 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 就介绍到这里,有什么不妥之处还请各位看官指正。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。