JavaScript - Promise
ES6 引入的一种异步编程方案,它会在未来的某个时间点交付异步操作的结果,这个结果既可以是成功的,也可以是失败的。
1. Promise 对象
一个Promise对象代表一次异步操作,且在生成后被立即执行
Promise对象有三种状态:
pending : 初始状态,既不是成功,也不是失败状态
fulfilled : 意味着操作成功完成
rejected : 意味着操作失败
通过new Promise()创建一个Promise对象
resolve将把 Promise 对象从pending变为fulfilled状态
reject将把 Promise 对象从pending变为rejected状态
const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 100); });
2. Promise 实例方法
then() 接受两个方法作为参数,分别用来指定成功状态与失败状态的回调,两个方法接受的参数由 Promise 对象中的resolve与reject提供。
p1.then( (val) => { console.log(val); }, (err) => { console.log(err); } );
catch() 处理异常的方法,参数为一个方法,与then的第二个回调方法类似,可以用于处理失败状态。不同点在于catch也可以获取到回调方法中的代码异常,所以实际使用时,经常使用以下写法。
p1.then((val) => { console.log(val); }).catch((err) => { console.log(err); });
finally() 在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。该方法没有参数。
p1.then(val => { console.log(val) }).catch(err => { console.log(err) }).finally() { console.log('end') }
3. Promise 静态方法
Promise.resolve() 创建一个立即fulfilled的Promise对象
Promise.resolve(1); /* 等价于 */ new Promise((resolve) => { resolve(1); });
Promise.reject() 创建一个立即rejected的Promise对象
Promise.reject(1); /* 等价于 */ new Promise((resolve, reject) => { reject(1); });
Promise.all() 参数为一个Promise数组,用于统一处理一组Promise对象的异步调用结果
当数组中所有的对象都为resolve时,新对象状态变为fulfilled
Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(val => { console.log(val) }).catch(err => { console.log(err) }) /** * output: * [1, 2] * /
当数组中有一个对象reject时,新对象变为rejected
Promise.all([Promise.resolve(1), Promise.reject(2), Promise.reject(3)]) .then(val => { console.log(val) }).catch(err => { console.log(err) }) /** * output: * 2 * /
Promise.race() 参数也为一个Promise对象数组,数组中有一个对象resolve时,新对象状态变为fulfilled;当有一个对象reject时,新对象状态变为rejected。race方法常用于超时处理。
const p1 = new Promise((resolve, reject) => { setTimeout(() => { reject(1); }, 500); }); const p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve(2); }, 100); }); Promise.race([p1, p2]) .then((val) => { console.log(val); }) .catch((err) => { console.log(err); }); /** * 由于p2的执行时间更短,所以race结果为fulfilled状态。 * output: * 2 */
4. 链式调用
在 2 中,then(),catch(),finally() 方法以链状的形式被先后调用,其能够执行的原因在于这三个方法本身都将 return 一个Promise实例,返回实例能够继续调用Promise的实例方法。
new Promise((resolve, reject) => {
resolve(1);
}).then(val => {
console.log(val)
return new Promise((resolve, reject) => {
resolve(2);
})
}).then(val => {
console.log(val)
return new Promise((resolve, reject) => {
resolve(3);
})
}).then(val => {
console.log(val);
})
/**
* output:
* 1
* 2
* 3
* /
5. Promise、setTimeout、async/await 执行顺序问题
a. js 事件循环机制(Event Loop)
在了解这三者的执行问题前,先来回顾一下 js 事件循环机制。众所周知,js 自古以来就是一门单线程,非阻塞的语言。单线程意味着只有一个主线程来执行所有的任务,也就是说同一时间只能执行一个任务。
Event Loop:分为主线程、宏任务、微任务三部分
主线程:可以直接执行的 js 代码
宏任务:setTimeout,setInterval
微任务:Promise.then(),new Promise主体部分的内容属于宏任务,可以直接执行。另外带 async 关键字的方法本身会返回一个 Promise 对象,可以理解为有 await 关键字的方法及其之前的部分为 Promise 对象的主体内容。带关键字await的方法之后的内容就是 Promise.then() 部分。
执行机制关键点:
优先执行主线程
遇到宏任务时,将其放入宏队列,遇到微任务时,将其放入微队列
主线程执行完毕后,执行微队列中的所有微任务直至清空
执行一个宏任务,宏任务完成后,查看主线程或微队列是否为空,若为空则继续执行
对于 setTimeout 而言,会在时间到了之后才会进入宏队列,而进入宏队列不意味着执行,也因此 setTimeout 内的代码的时机执行间隔是随机且略大于设定时间的。
了解了机制之后,举一个例子
1 async function async1(){ 2 console.log('async1 start') 3 await async2() 4 console.log('async1 end') 5 } 6 async function async2(){ 7 console.log('async2') 8 } 9 console.log('script start') 10 setTimeout(function(){ 11 new Promise(function(resolve){ 12 console.log('promise3') 13 resolve() 15 }).then(function(){ 16 setTimeout(function(){ 17 console.log('setTimeout3') 18 }, 0) 19 console.log('promise4') 20 }) 21 console.log('setTimeout1') 22 },0) 23 setTimeout(function(){ 24 console.log('setTimeout2') 25 }, 0) 26 async1(); 27 new Promise(function(resolve){ 28 console.log('promise1') 29 resolve(); 30 }).then(function(){ 31 console.log('promise2') 32 }) 33 console.log('script end')
挨个进行分析
首先主线程从 9 开始执行,毫无疑问先输出 script start;
10~22,23~25。两个 setTimeout 方法,宏任务,由于主线程还没执行结束,所以先放入宏队列,分别记为 M1 和 M2;
26 为带 async 关键字的方法,先进入,2~3 为主线程代码,所以输出 async1 start,然后执行 async2();
async2()这个方法的 async 关键字在这个例子中其实是多余的,该方法可以视作一个普通的方法,所以为主线程内容,输出 async2;
4~5 为 await 之后,相当于 Promise.then 部分,主线程还没结束,扔进微队列,记为 m1;
主线程现在到了 27,28 为 Promise 对象主体,主线程内容,输出 promise1;
30~32,Promise.then 部分,扔进微队列,记为 m2;
33,主线程,输出 script end,到此主线程暂时执行完毕;
主线程完毕,开始看微队列,先进先出原则,首先执行 m1,输出 async1 end;
微队列没有结束,继续执行微队列中的 m2,故输出 promise2,到此微队列暂时也告一段落;
接下来开始执行宏队列中的 M1,11~12 相当于主线程代码块,直接运行,输出 promise3;
13 为 resolve 方法,执行后将 15~20 扔进微队列,记为 m3;
21 为当前宏任务主代码块内容,直接输出 setTimeout1。至此,M1 宏任务结束;
此时,微队列增加 m3,故要先去执行微队列,发现 16~18 又是一个宏任务,记为 M3;
直接执行 19,输出 promise4;
微队列再次清空,执行宏队列中的 M2,输出 setTimeout2;
一个宏任务结束,查看主线程与微队列都为空,继续执行宏队列的 M3,输出 setTimeout3,至此,代码全部执行完成。
执行结果
script start async1 start async2 promise1 script end async1 end promise2 promise3 setTimeout1 promise4 setTimeout2 setTimeout3
结语: 代码执行顺序问题引发的 bug 在平时工作中经常出现,了解 js 代码以及各个异步编程方法的执行机制,可以有效规避这类问题。
最后更新于
这有帮助吗?