Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。
1.异步、同步
如果你没有接触过Promise
,或者执行异步请求操作,开头这句话看起来很难理解,我们首先需要了解什么是异步,什么是同步,并了解我们为什么需要异步。
JavaScript是单线程语言,所谓"单线程",就是指一次只能完成一件任务。而异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果。
同步和异步的区别:
- 同步模式:后一个任务等待前一个任务结束,然后再执行,如果遇到等待会阻塞代码的执行;
- 异步模式:后一个任务不等待前一个任务结束就可以执行,不会阻塞代码的执行。异步通过callback(回调)形式调用。
具体来说,异步运行机制如下:
- 所有同步任务都在主线程上排队依次执行,形成一个执行栈。
- 主线程之外,还存在一个"任务队列"(task queue)。异步任务进入Event Table并注册函数,异步任务有了运行结果,就在“任务队列”之中放置一个事件。
- 一旦主线程中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入主线程执行。
- 主线程内的任务执行完毕为空,就会从"任务队列"中读取事件,这个过程是循环不断的,形成event loop(事件循环)
//异步演示 通过回调的形式调用,不会阻塞代码的执行
console.log(1)
setTimeout(()=>{
console.log(2)
},1000)
console.log(3) // 输出结果 1,3,2
//同步演示 必须点击确认才能继续输出‘3’
console.log(1)
alert(2)
console.log(3)
2.异步调用问题的几个阶段
最常见的异步调用就是ajax技术,但在使用异步的时候就会面临一个问题,我们不能控制代码的执行顺序,有时我们需要使用异步调用返回的数据,但我们无法得知调用何时结束,以下的技术实质上都是为了解决这一问题产生的。
主要阶段:回调函数–>Promise–>Generator–>async/await
2.1回调函数阶段
所谓的回调函数就是一个被当做参数传递到另一个函数中,且在适当的时候被调用的函数。
例:现在我们有一个被分成三步的操作,每一步都依赖于上一步,在这个例子中,第一步需要获取当前省份,第二步需要获取当前省份的全部市,第三步获取上一步中获取市的第一个市的所有县/区,并打印所有的结果。
注:$.get函数指的是封装的method为get的ajax函数
//获取当前省份函数
function getCurProvince(callback){
$.get(api+'/getCurProvince',(data)=>{
callback(data);
});
}
//获取当前省份所有城市的函数
function getCitysByProvice(province,callback){
$.get(api+'/getCitysByProvice?province='+province,(data)=>{
callback(data);
});
}
//获取当前省份所有城市中第一个城市所有区县的函数
function getCountyByFirstCity(citys,callback){
$.get(api+'/getCountyByFirstCity?firstCity='+citys[0],(data)=>{
callback(data);
});
}
//打印当前省份及其所有城市及第一个城市所有区县的函数
function getLocations(){
getCurProvince((province)=>{
console.log(province);
getCitysByProvice(province,(citys)=>{
console.log(citys);
getCountyByFirstCity(citys,(countys)=>{
console.log(countys);
});
});
});
}
getLocations();
因为必须在回调函数中调用回调函数,我们就得到了这个深度嵌套的 getLocations() 函数,这就更难阅读和调试了。这就是我们常说的callback hell(回调地狱)。
所以在大多数现代异步 API 中都不使用回调。事实上,JavaScript 中异步编程的基础是Promise。
2.2Promise阶段
在回调函数中,我们需要在参数列表传入函数,这就会导致嵌套的发生,也是回调地狱产生的原因。而Promise正是针对这一问题产生的。
本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了,而是可以采用Promise风格的编码方式,链式调用。
通过Promise我们可以将上面的getLocations()函数优化一下
//将原始函数改造成Promise函数
//获取当前省份函数
function getCurProvince(){
return new Promise((resolve, reject) => {
$.get(api+'/getCurProvince',(data)=>{
resolve(data)
})
});
}
//获取当前省份所有城市的函数
function getCitysByProvice(province){
return new Promise((resolve, reject) => {
$.get(api+'/getCitysByProvice?province='+province,(data)=>{
resolve(data);
})
})
}
//获取当前省份所有城市中第一个城市所有区县的函数
function getCountyByFirstCity(citys){
return new Promise((resolve, reject) => {
$.get(api+'/getCountyByFirstCity?firstCity='+citys[0],(data)=>{
resolve(data);
});
})
}
//打印当前省份及其所有城市及第一个城市所有区县的函数
function getLocations(){
getCurProvince().then((province)=>{
console.log(province)
return getCitysByProvice(province)
})
.then((citys)=>{
console.log(citys)
return getCountyByFirstCity(citys)
})
.then((data)=>{
console.log(data)
})
}
getLocations();
上面的代码看起来更多了,但可读性和可调试性大大增强,Promise链式调用的风格解决了回调地狱的问题。
如上代码中关于Promise相关的知识请阅读第三章Promise
2.3Generator
Promise风格的链式调用虽然解决了回调地狱的问题,但是Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator是为JavaScript设计的一种轻量级的协程。它通过yield关键字,可以控制一个函数暂停或者继续执行。Generator在处理异步时,更像同步模式。我们同样用Generator的方式改写一下上面的getLocations()函数
//获取当前省份函数
function getCurProvince(){
$.get(api+'/getCurProvince',(data)=>{
return data
});
}
//获取当前省份所有城市的函数
function getCitysByProvice(province){
$.get(api+'/getCitysByProvice?province='+province,(data)=>{
return data
});
}
//获取当前省份所有城市中第一个城市所有区县的函数
function getCountyByFirstCity(citys){
$.get(api+'/getCountyByFirstCity?firstCity='+citys[0],(data)=>{
return data
});
}
//打印当前省份及其所有城市及第一个城市所有区县的函数
function* getLocationsGen(){
const province = yield getCurProvince();
console.log(provice);
const citys = yield getCitysByProvice(province);
console.log(citys);
const countys = yield getCountyByFirstCity(citys);
console.log(countys)
}
function getLocations(){
}
const generator=getLocations()
getLocations();
Generator函数是将函数分步骤阻塞 ,只有主动调用next() 才能进行下一步 ,因为asyns函数相当于Generator函数的语法糖,做出了优化,所以这里对Generator函数不做赘述,而且一般用到异步编程的时候一般也只用async和promise。所以这里就省略了。但某些情况下Generator函数也非常重要,比如蚂蚁金服的dva中,异步操作的同步表达就使用的Generator,有兴趣的同学可以去单独了解一下。
2.4async/awiat
简单的说async函数就相当于自执行的Generator函数,相当于自带一个状态机,在await的部分等待返回, 返回后自动执行下一步。
而且相较于Promise,async的优越性就是把每次异步返回的结果从then中拿到最外层的方法中,不需要链式调用,只要用同步的写法就可以了。更加直观而且,更适合处理并发调用的问题。但是async必须以一个Promise对象开始 ,所以async通常是和Promise结合使用的
//将原始函数改造成Promise函数
//获取当前省份函数
function getCurProvince(){
return new Promise((resolve, reject) => {
$.get(api+'/getCurProvince',(data)=>{
resolve(data)
})
});
}
//获取当前省份所有城市的函数
function getCitysByProvice(province){
return new Promise((resolve, reject) => {
$.get(api+'/getCitysByProvice?province='+province,(data)=>{
resolve(data);
})
})
}
//获取当前省份所有城市中第一个城市所有区县的函数
function getCountyByFirstCity(citys){
return new Promise((resolve, reject) => {
$.get(api+'/getCountyByFirstCity?firstCity='+citys[0],(data)=>{
resolve(data);
});
})
}
//打印当前省份及其所有城市及第一个城市所有区县的函数
async function getLocations(){
const province=await getCurProvince()
console.log(province)
const citys=await getCitysByProvice(province)
console.log(citys)
const countys=await getCountyByFirstCity(citys)
console.log(countys)
}
getLocations();
从上面的代码我们可以看到getLocations
函数使用async/await相比链式调用的Promise,更像同步模式,而且传参更加方便,异步顺序更加清晰
关于async函数以及await关键字的相关知识请阅读第四章async/await
3.Promise
到这里我们终于可以解释文章开头的那句话,Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。
一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知值的代理。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。
一个 Promise 必然处于以下几种状态之一:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功完成。
- 已拒绝(rejected):意味着操作失败。
待定状态的 Promise 对象要么会通过一个值_被兑现_,要么会通过一个原因(错误)被拒绝。当这些情况之一发生时,我们用 promise 的 then 方法排列起来的相关处理程序就会被调用。如果 promise 在一个相应的处理程序被绑定时就已经被兑现或被拒绝了,那么这个处理程序也同样会被调用,因此在完成异步操作和绑定处理方法之间不存在竞态条件。
3.1优势
我们总结下通过对于异步调用问题几个阶段对比后,promise这种风格的异步处理方式有何优势:
- 在本轮时间循环运行完成之前,回调函数是不会被调用的。
- 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
- 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。
3.2链式调用
连续执行两个或者多个异步操作是一个常见的需求,在上个操作执行成功后,开始下一个操作,并可能会使用到上一步异步操作返回的结果。当我们使用回调函数去解决这一类问题时,就会出现回调地狱的情况,而使用promise的链式调用可以很好的解决这一问题,在之前的例子中也可以看出链式调用的优势。
我们可以用 Promise.prototype.then()
、Promise.prototype.catch()
和 Promise.prototype.finally()
这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。
例如.then()
方法需要两个参数,第一个参数作为处理已兑现状态的回调函数,而第二个参数则作为处理已拒绝状态的回调函数。每一个 .then()
方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用,就像这样:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
//handleResolved和handleRejected分别为异步函数执行成功或者失败后的执行函数
myPromise
.then(handleResolved1, handleRejected1)
.then(handleResolved2, handleRejected2)
.then(handleResolved3, handleRejected3);
当.then()
中缺少能够返回 promise 对象的函数时,链式调用就直接继续进行下一环操作。因此,链式调用可以在最后一个.catch()
之前把所有的处理已拒绝状态的回调函数都省略掉。
过早地处理变为已拒绝状态的 promise 会对之后 promise 的链式调用造成影响。不过有时候我们因为需要马上处理一个错误也只能这样做。例如,外面必须抛出某种类型的错误以在链式调用中传递错误状态。另一方面,在没有迫切需要的情况下,可以在最后一个.catch()
语句时再进行错误处理,这种做法更加简单。.catch()
其实只是没有给处理已兑现状态的回调函数预留参数位置的 .then()
而已。
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);
这些函数的终止状态决定着链式调用中下一个 promise 的“已敲定”状态是什么。“已决议”状态意味着 promise 已经成功完成,而“已拒绝”则表示 promise 未成功完成。“已决议”状态的返回值会逐级传递到下一个 .then()
中,而“已拒绝”的理由则会被传递到链中的下一个已拒绝状态的处理函数。
链式调用中的 promise 们就像俄罗斯套娃一样,是嵌套起来的,但又像是一个栈,每个都必须从顶端被弹出。链式调用中的第一个 promise 是嵌套最深的一个,也将是第一个被弹出的。
3.3构造函数与静态方法
3.3.1构造函数
Promise()
创建一个新的 Promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。
3.3.2静态方法
Promise.all(iterable)
这个方法返回一个新的 promise 对象,等到所有的 promise 对象都成功或有任意一个 promise 失败。如果所有的 promise 都成功了,它会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值。顺序跟 iterable 的顺序保持一致。一旦有任意一个 iterable 里面的 promise 对象失败则立即以该 promise 对象失败的理由来拒绝这个新的 promise。Promise.allSettled(iterable)
等到所有 promise 都已敲定(每个 promise 都已兑现或已拒绝)。返回一个 promise,该 promise 在所有 promise 都敲定后完成,并兑现一个对象数组,其中的对象对应每个 promise 的结果。Promise.any(iterable)
接收一个 promise 对象的集合,当其中的任意一个 promise 成功,就返回那个成功的 promise 的值。Promise.race(iterable)
等到任意一个 promise 的状态变为已敲定。当 iterable 参数里的任意一个子 promise 成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应处理函数,并返回该 promise 对象。Promise.reject(reason)
返回一个状态为已拒绝的 Promise 对象,并将给定的失败信息传递给对应的处理函数。Promise.resolve(value)
返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行结果决定;否则,返回的 Promise 对象状态为已兑现,并且将该 value 传递给对应的 then 方法。通常而言,如果你不知道一个值是否是 promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 promise 对象形式使用。
3.4实例方法
Promise.prototype.catch()
为 promise 添加一个被拒绝状态的回调函数,并返回一个新的 promise,若回调函数被调用,则兑现其返回值,否则兑现原来的 promise 兑现的值。Promise.prototype.then()
为 promise 添加被兑现和被拒绝状态的回调函数,其以回调函数的返回值兑现 promise。若不处理已兑现或者已拒绝状态(例如,onFulfilled 或 onRejected 不是一个函数),则返回 promise 被敲定时的值。Promise.prototype.finally()
为 promise 添加一个回调函数,并返回一个新的 promise。这个新的 promise 将在原 promise 被兑现时兑现。而传入的回调函数将在原 promise 被敲定(无论被兑现还是被拒绝)时被调用。
4.async&await
4.1定义与使用
在 JavaScript 中,async 和 await 是用于处理异步操作的关键字。它们提供了一种更加简单、可读性更高的方式来处理异步操作,避免了回调函数嵌套和 Promise 链式调用的问题。在这篇文章中,我们将介绍 async 和 await 的基本概念、用法和一些常见的问题。
什么是 async 和 await?
async 和 await 是 JavaScript 中的两个关键字。async 关键字用于定义一个异步函数,而 await 关键字用于等待异步操作的结果。
异步函数是一个返回 Promise 的函数。在函数内部,我们可以使用 await 关键字来等待异步操作的结果,而不需要使用回调函数或者 Promise 链式调用。
async 函数的基本用法
async 函数的声明方式与普通函数类似,只需要在函数声明前加上 async 关键字即可。下面是一个简单的 async 函数示例:
async function fetchData() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
return data;
}
fetchData().then(data => console.log(data));
在这个示例中,我们定义了一个异步函数 fetchData,它使用 await 关键字等待 fetch 方法返回的 Promise 对象,然后使用 await 关键字等待 response 对象的 json() 方法返回的 Promise 对象。最后,我们将返回的数据传递给 then() 方法来处理它。
4.2异步函数的错误处理
在异步函数中,我们可以使用 try-catch 语句来处理错误。如果异步操作失败了,它将抛出一个错误,并被 catch 语句捕获。
下面是一个示例:
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
return data;
} catch (error) {
console.log(`Error: ${error.message}`);
}
}
fetchData();
在这个示例中,我们使用 try-catch 语句来捕获 fetch 方法和 json() 方法可能抛出的错误。如果异步操作失败了,它将抛出一个错误,并被 catch 语句捕获。我们可以在 catch 语句中处理这个错误。
4.3异步函数的并行执行
使用 Promise.all() 方法可以让多个异步操作并行执行。Promise.all() 方法接受一个包含多个 Promise 对象的数组,并返回一个新的 Promise 对象,它将在所有 Promise 对象都成功完成后解决。
下面是一个示例:
async function fetchData() {
const promise1 = fetch('https://jsonplaceholder.typicode.com/todos/1');
const promise2 = fetch('https://jsonplaceholder.typicode.com/todos/2');
const [response1, response2] = await Promise.all([promise1, promise2]);
const data1 = await response1.json();
const data2 = await response2.json();
return [data1, data2];
}
fetchData().then(data => console.log(data));
在这个示例中,我们定义了两个 Promise 对象,使用 Promise.all() 方法将它们合并成一个 Promise 对象,并使用解构赋值将两个 response 对象分别赋值给 response1 和 response2。然后,我们使用 await 关键字等待每个 response 对象的 json() 方法返回的 Promise 对象,并将它们存储在 data1 和 data2 变量中。
async 和 await 提供了一种更加简单、可读性更高的方式来处理异步操作。使用它们可以避免回调函数嵌套和 Promise 链式调用的问题,使代码更加清晰易懂。在使用 async 和 await 时,我们需要注意错误处理和并行执行等方面的问题。