最近在摸鱼的时候,遇到了一个需求:用户可以接连上传多张图片,但是uni.upload一次只能上传一张图片,我当时想着是设置一个Promise,在一个循环中挨个上传,最后做一个整体resolve,但是这样并不合理。后来听得说是可以使用Promise.all方法进行多次请求,但是我只是略懂皮毛。因此决定写下几篇文章,好好学一学Promise的功能与诸多方法。本文将着重介绍Promise的执行顺序和宏任务、微任务概念。
首先了解一些基本概念。
在一个代码段中,我们可以写下输出、赋值等基本语句作为整个脚本的宏任务,也可以写下setTimeout、setInterval等和时间相关的宏任务、还可以通过Promise.then创建微任务。这些任务也有一个相应的优先级:
script语句作为基本宏任务优先直接执行,初次遇到setTimeout等代码将推入宏任务队列,初次遇到promise的内容时,promise内部代码是同步的,因此作为script语句的宏任务正常直接执行,它的.then或者时.catch方法将被压入微任务队列。不过,只有当promise状态不为pedding的时候才可以成功压入队列。
整体流程说来就是:我们输入完成一个代码段后进行运算时,赋值、输出等基本语句以及Promise内部代码时从上往下依次执行,遇到其他宏任务时推入宏任务队列,遇到微任务时推入微任务队列。当一段宏任务代码执行完毕后查看微任务队列中是否有任务,如果有则优先执行全部的微任务,之后再执行下一条宏任务,这段宏任务执行完毕后再检查微任务,再...........循环往复,直至所有任务执行完毕。
了解了基本概念后我们可以看几道例题加深印象。
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
})
promise1.then(() => {
console.log(2);
});
console.log('1', promise1);
代码段从上至下,优先执行script语句中new Promise中的同步代码,再执行下面的输出1,由于Promise并未做任何处理,状态还是pedding,因此promise.then并未执行。
最终结果为:
'promise1'
'1' Promise{}
const fn = () => (new Promise((resolve, reject) => {
console.log(1);
resolve('success')
}))
fn().then(res => {
console.log(res)
})
console.log('start')
代码段从上至下,首先定义乐了一个函数fn,fn的返回值是一个promise,注意这里是定义,并非执行,所以没有任何输出。然后执行到fn().then语句,需要注意的是,这里是fn(),意思是执行fn函数,那么开始调用fn,输出1并将返回的promise状态改为resolved。在看到.then方法,推入微任务队列,接着执行下面的start输出。执行至此,第一个宏任务执行完毕,接下来查看微任务队列,发现有一个任务输出success微任务队列清空,再查找宏任务队列中没有任务,执行完毕。
最终结果为:
1
'start'
'success'
console.log('start')
setTimeout(() => {
console.log('time')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
代码段从上至下,先输出start,遇到setTimeout时将其推入宏任务队列,遇到Promise.resolve().then()的时候将其推入微任务队列,然后输出end。当前宏任务执行完毕,开始检查有无微任务,有,输出resolve。清空微任务队列后,再检查宏任务队列有无任务,有,输出time。该宏任务执行完毕后检查有无微任务,无;再检查有无宏任务,无,执行完毕。
最终结果为:
'start'
'end'
'resolve'
'time'
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
代码从上至下,牢记!Promise内部的代码仍然是同步执行代码,它的then和catch才会被推入微任务队列。因此输出1,将setTimeout推入宏任务队列,再输出2。接下来看到Promise.then,然而我们此时并不能将其推入微任务队列,因为它此时还是pedding状态,定义它的promise还并未执行resolve或者是reject,先不执行它。因此接下来输出的是4。接下来查看微任务队列,无,再查看宏任务,有,输出timerStart,将该promise状态改为resolved并将之前的promise.then
推入微任务队列,输出timerEnd。该宏任务执行完毕。执行完一段宏任务后查看微任务队列,有,输出success。
最终结果为:
1
2
4
"timerStart"
"timerEnd"
"success"
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
console.log("timer1");
}, 1000);
console.log("promise1里的内容");
});
const promise2 = promise1.then(() => {
throw new Error("error!!!");
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("timer2");
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);
需要注意的是,这里和1.1.2有所不同。在1.1.2中代码是const fn = () => (new Promise(),意思是在调用fn时返回一个promise,但在这里是promise1 = new promise(),根据之前介绍的,这里promise里的同步代码正常进行。
代码从上至下,遇到setTimeout,放入宏任务队列,接下来输出"promise1里的内容";再往下,promise2这一行要使用到promise1.then,由于1的状态是pedding,因此无法执行。再往下,输出"promise1
在执行完一段宏任务后检查是否有微任务,无,执行宏任务队列中第一个任务。因此将promise1的状态就改为resolved并将之前的promise2推入微任务队列,再输出timer1。该宏任务执行完毕,查看微任务队列,有,进行err的输出。微任务队列全部执行完毕后,执行下一个宏任务队列,输出timer2,"promise1
最终结果为:
'promise1里的内容'
'promise1' Promise{}
'promise2' Promise{}
'timer1'
test5.html:102 Uncaught (in promise) Error: error!!! at test.html:102
'timer2'
'promise1' Promise{: "success"}
'promise2' Promise{: Error: error!!!}
进行这部分的题目练习前需要看一些概念。
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => {
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})
promise的状态只能改变一次,因此执行了reject后便不会执行接下来的resolve。又因为catch
不管被连接到哪里,都能捕获上层未捕捉过的错误,于是接来下进入catch方法进行错误的捕获。在执行完catch后会接着执行下面的then3,因为.then
和.catch
都会返回一个新的Promise。但是由于catch返回的promise没有返回值,所以打印出来的是undefined。
最终结果为:
"catch: " "error"
"then3: " undefined
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer')
resolve('success')
}, 1000)
})
const start = Date.now();
promise.then(res => {
console.log(res, Date.now() - start)
})
promise.then(res => {
console.log(res, Date.now() - start)
})
Promise
的 .then
或者 .catch
可以被调用多次,但这里 Promise
构造函数只执行一次。或者说 promise
内部状态一经改变,并且有了一个值,那么后续每次调用 .then
或者 .catch
都会直接拿到该值。 如果足够快的话,也可能做到两个都是1001。
最终结果为:
'timer'
'success' 1001
'success' 1002
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
也许这看起来像是在原promise中返回了一个Error,然后执行catch。但是,不要被表面的Error迷惑了,这里是return Error,返回任意一个非 promise
的值都会被包裹成 promise
对象,因此这里的return new Error('error!!!')
也被包裹成了return Promise.resolve(new Error('error!!!'))
。
最终结果为:
"then: " "Error: error!!!"
所以,如果是想要抛出一个错误,推荐用一下两种方法
return Promise.reject(new Error('error!!!'));
// or
throw new Error('error!!!')
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally1'))
promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally2'))
解析:
这里主要想介绍如果是连续的.then方法,在一次宏任务中只会执行一次,在执行完第一个then方法后,后续的then会被推入微任务队列中,可以理解为链式调用后面的内容需要等前一个调用执行完才会执行,就像是这里的finally()
会等promise1().then()
执行完才会将finally()
加入微任务队列。
最终结果为:
'promise1'
'1'
'error'
'finally1'
'finally2'
通俗来说,.all()
的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调;.race()
的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log(res))
假设现在要处理三个数据的Promise,首先定义了一个函数renAsync,然后在这个promise里面抽象化某一个数据的执行方法。然后进入Promise.all语句。根据概念可知,all方法里面可以包裹多个Promise,因此将多个Promise放在数组[runAsync(1), runAsync(2), runAsync(3)]中,最后可以将他们的结果以数组的格式展现出来。有了all,我们就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
最终结果为:
// 在间隔一秒后同时打印出1,2,3,还有一个数组[1,2,3]
1
2
3
[1, 2, 3]
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
代码从上往下看,定义了两个函数先不用管,看到调用了async1,执行。输出async1 start后看到await,意思是等待async2函数的执行,因此输出async2。需要注意,在这执行完后跳出async1,执行宏任务的同步代码start。在这一轮宏任务执行完毕后再来执行await后面的async1 end。我们可以理解为紧跟着await后面的语句相当于放到了new Promise中(await async2),下一行及之后的语句相当于放在Promise.then中(console.log(async1 end))
async function async1() {
console.log("async1 start");
// 原来代码
// await async2();
// console.log("async1 end");
// 转换后代码
new Promise(resolve => {
console.log("async2")
resolve()
}).then(res => console.log("async1 end"))
}
async function async2() {
console.log("async2");
}
async1();
console.log("start")
最终结果为:
'async1 start'
'async2'
'start'
'async1 end'
如果将这里的await async2换成一个new Promise
async function async1() {
console.log("async1 start");
new Promise(resolve => {
console.log('promise')
})
console.log("async1 end");
}
async1();
console.log("start")
'async start'
'promise'
'async1 end'
'start'
我们可以清晰的看到,promise并不会阻塞后面同步代码end的执行。对比一下我们可以加深对await的理解:await的目标函数会被转化为一个new promise,该行语句的后面语句将会作为.then方法推入微任务队列中执行。需要注意的是,await语句的末尾在转换过后要添加上resolve()来改变.then方法中的状态。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log("async2");
}
async1();
console.log("start")
和题目一差不多类型,这次添加上了定时器,我们还是可以将这个await换成promise理解。
async function async1() {
console.log("async1 start");
new Promise(resolve=>{
setTimeout(() => {
console.log('timer')
}, 0)
resolve()
console.log("async2");
}).then(()=>{
console.log("async1 end");
})
async1();
console.log("start")
当async1执行的时候首先输出start,遇到promise的同步任务,进去执行。遇到setTimeout,推入宏任务队列,继续往下遇到async2输出。之后再接着script语句的宏任务往下走输出start。这一轮宏任务执行完毕查看微任务队列,遇到已经被resolve后的then,输出end。再查看宏任务队列,发现定时器的任务,输出timer。
最终结果为:
'async1 start'
'async2'
'start'
'async1 end'
'timer'
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
这里做一道综合一点的题,也许一时半会还不能肉眼判别,所以还是来进行转换。
async function async1() {
console.log("async1 start");
new Promise(resolve=>{
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
resolve()
}).then(resolve=>{
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
})
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
最终结果为:
'async1 start'
'async2'
'start'
'async1 end'
'timer2'
'timer3'
'timer1'
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise1 resolve')
}).then(res => console.log(res))
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
这里将计时器换成了一个promise,我们再来转换看看。
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise1 resolve')
}).then(res => console.log(res))
.then(resolve=>{
console.log('async1 success');
resolve()
return 'async1 end'
})
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
最终结果为:
'script start'
'async1 start'
'promise1'
'script end'
'promise1 resolve'
'async1 success'
'async1 end'
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise resolve')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
console.log(res)
})
new Promise(resolve => {
console.log('promise2')
setTimeout(() => {
console.log('timer')
})
})
继续转换。
async function async1 () { // 2
console.log('async1 start'); // 3
await new Promise(resolve => {
console.log('promise1') // 4
resolve('promise resolve') // 5 转换该promise的状态
}).then(resolve=>{ // 6 推入微任务队列 //10 第一轮宏任务结束,执行该微任务
console.log('async1 success'); // 11
return 'async1 end' //12 返回新的promise.resolve('async1 end')
})
}
console.log('srcipt start') // 1
async1().then(res => { // 2 调用async1() //7 pedding状态保持不变,不推入队列
console.log(res) //13 状态改变为resolve
})
new Promise(resolve => {
console.log('promise2') // 8
setTimeout(() => { // 9 推入宏任务队列
console.log('timer') // 14
})
})
最终结果为:
'script start'
'async1 start'
'promise1'
'promise2'
'async1 success'
'async1 end'
'timer'
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
还是继续转换。
async function async1() { // 3
console.log("async1 start"); // 4
await async2(); // 5
console.log("async1 end"); // 6 推入微任务队列 // 11
}
async function async2() {
console.log("async2"); // 5 同步执行
}
console.log("script start"); // 1
setTimeout(function() { // 2 推入宏任务队列
console.log("setTimeout"); // 13
}, 0);
async1(); // 3
new Promise(function(resolve) {
console.log("promise1"); // 7
resolve(); // 8 改变当前promise状态
}).then(function() { // 9 推入微任务队列
console.log("promise2"); // 12
});
console.log('script end') // 10
当你做出了前面的题目后再来看这道题,会感觉有些轻松。不过这道题是头条的一道面试题喔。
为这一系列的题目收个尾。
async function testSometing() {
console.log("执行testSometing");
return "testSometing";
}
async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing();
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise(resolve => {
console.log("promise start...");
resolve("promise");
});
promise.then(val => console.log(val));
console.log("test end...");
也许熟练了可以直接答出来,不过我们这里还是做一个转换。
async function test() { // 1
console.log("test start..."); // 2
const v1 = new Promise(resolve=>{
console.log("执行testSometing"); // 3
return "testSometing"; // 4 将当前promise状态改为resolve
resolve()
}).then(res=>{ // 5 推入微任务队列
console.log(v1); // 10
const v2 = new Promise(resolve=>{
console.log("执行testAsync"); // 11
return Promise.resolve("hello async"); // 12
resolve()
}).then(res=>{ // 13 推入微任务队列
console.log(v2); // 15
console.log(v1, v2); // 16
})
})
}
test(); // 1
var promise = new Promise(resolve => {
console.log("promise start..."); // 6
resolve("promise"); // 7 将当前promise状态改为resolve
});
promise.then(val => console.log(val)); // 8 推入微任务队列 // 14
console.log("test end..."); // 9
最好的理解方式就是看文档或者做题,当把这些题刷完后对于promise的理解可以更加深刻。
参考文章:掘金