最近被JS的异步执行折磨到怀疑人生,终于把这个硬骨头啃下来了,记录下我学习异步执行的心路历程。
我们为什么要用到异步执行,因为JS的执行环境是单线程的,如果有多个任务,就必须排队,只要有一个任务耗时很长,后面的任务都必须排队等着,其他任务无法执行。所以,JS将任务的执行模式分成两种:同步和异步,异步模式下,每一个任务有一个或多个回调函数,前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,这也是为什么异步执行再难,我们也要把它啃下来。
一. 异步执行的方法有哪些?
有5种
- 1.回调函数
- 2.Promise对象
- 3.Async函数
- 4.事件监听
- 5.发布订阅
因为今天是ES6的主场,所以我们今天主要介绍两种:Promise对象和Async函数。
为什么ES6会新增Promise对象呢,是因为回调函数的会出现回调地狱的情况,可以看我的另一篇文章开头部分juejin.cn/post/687673…, 有详细的介绍。
二. 串行并行同步异步阻塞非阻塞
我们用一个案例来形象的理解这堆抽象的名词。
案例:有一个老大,派了5个小弟去银行帮他取钱,小弟有可能取钱成功,也有可能取钱不成功,每个小弟取钱完成的时间也不一样。我们讨论下面这三种情况:
同步串行阻塞: 每个小弟去银行取了号之后就坐在那里啥也不干,等叫号办理(同步的),每个小弟按顺序去银行(串行),前一个小弟如果取钱失败,则重新进银行取钱,如果成功,则下一个小弟进银行办理(阻塞)
异步串行阻塞: 每个小弟去银行取了号之后,就去干别的事情了,等听到叫号他才回来继续办理(异步),每个小弟按顺序去银行(串行),前一个小弟如果取钱失败,则重新进银行取钱,如果成功,则下一个小弟进银行办理(阻塞)
异步并行非阻塞: 每个小弟去银行取了号之后,就去干别的事情了,等听到叫号他才回来继续办理(异步),所有小弟一起去银行(并行),后一个小弟不需要等前一个小弟完成才能办理(非阻塞)
1. 同步串行阻塞
- 小弟执行是同步的
- 后面小弟要等前面的小弟完成之后才能办理业务
const total = 5; // 共5个小弟
//模拟小弟成功或者失败(同步)
function passOrFailed(i){
console.log(`=======第${i}个小弟去取钱=======`)
return (Math.random() < 0.7); // 小弟成功的概率是70%
}
//小弟按顺序排队进银行取钱
function doActionWithES5(total){
for (var i = 0; i<total; i++){
if(passOrFailed(i) == true){
console.log(`${i}成功,轮到下一个小弟`);
}else{
console.log(`${i}失败,重试`);
i --;
}
}
console.log('All Finish!!!')
}
doActionWithES5(total);
输出:
2. 异步串行阻塞
实际应用中更多的是异步的情况。
上面代码中,如果我们只把模拟的函数变成异步,串行阻塞部分不变,实际上并不能实现效果,你们能看出来下面代码有什么问题吗?
const total = 5;
//模拟小弟成功或者失败(异步)
function passOrFailed(i){
console.log(`=======模拟第${i}个操作=======`)
// 异步
setTimeout(() => { // a
return (Math.random() < 0.7);
}, Math.floor(Math.random()*1000)); //每个小弟完成的时间不一样
}
//小弟按顺序排队进银行取钱(串行阻塞)
function doActionWithES5(total){
for (var i = 0; i<total; i++){
if(passOrFailed(i) === true){ // b
console.log(`${i}成功,执行下一个`);
}else{
console.log(`${i}失败,重试`); // c
i --;
}
}
console.log('All Finish!!!');
}
doActionWithES5(total);
输出结果是这样的,他会一直无限循环第0个操作,并且每次都是失败,我把程序打了断点逐步输出,不然会进行死循环,电脑资源会被用完:
**原因:**标记a处的的timeout是异步的,所以标记b处一直拿不到返回,从而一直跳到标记c处,进入死循环,一直轮不到异步队列里的任务执行。
ES6里的Promise对象可以很好的解决这种情况,到这里需要先看我的另一篇文章先了解Promise对象juejin.cn/post/687673… ,不然后面可能会比较难理解。
2.1异步串行阻塞: (ES6:Promise对象)
- 把异步操作放到promise对象里,promise对象就可得到当前异步操作的对象,且有了对应的状态。
- 有了状态,我们才能判断小弟是否取完钱了,才能依赖状态决定下一个小弟是否可以开始去取钱了。
const total = 5;
// 模拟小弟成功或者失败(异步)
function passOrFailed(i){
return new Promise((resolve, reject) => {
console.log(`=======模拟第${i}个小弟去取钱=======`)
// 异步操作
setTimeout(() => {
if (Math.random() < 0.7){
console.log(`${i}成功`);
resolve();
}else{
console.log(`${i}失败`);
reject();
}
}, Math.floor(Math.random()*1000)); //每个任务完成的时间不定
})
}
// 小弟按顺序排队进银行取钱(串行阻塞)
// 失败重试,用到递归
function doActionWithPromise(current){
if(current < total){
let p = passOrFailed(current);
p.then(() => {
// 成功,轮到下一个小弟
doActionWithPromise(current+1);
}, () => {
// 失败,当前小弟再做一次(递归)
doActionWithPromise(current);
})
}else {
// 当全部小弟都完成
console.log('All Finish!!!');
}
}
doActionWithPromise(0); // 从第0个小弟开始
输出:
2.2 异步串行阻塞:(ES6: Async-await)
async 修饰函数定义:
-
1. 函数会天然返回一个Promise对象,如果不是,用Promise.resolve加工 -
2. 一旦一个函数用async,那么调用它的方式是异步的。 -
3. 返回的Promise对象,在函数执行完是fulFilled,如果任意一个await reject,那么整个函数reject。
await 在函数调用前:
-
1. 被调用函数应当返回一个Promise对象 -
2. 在函数内部,await会阻塞函数的执行,直到Promise状态发生变化
const total = 5;
// 模拟小弟取钱的函数
function passOrFailed(i){
return new Promise((resolve, reject) => {
console.log(`=======模拟第${i}个小弟去取钱=======`)
// 异步操作
setTimeout(() => {
if (Math.random() < 0.7){
console.log(`${i}成功`);
resolve();
}else{
console.log(`${i}失败`);
reject();
}
}, Math.floor(Math.random()*1000)); //每个任务完成的时间不定
})
}
// 小弟取钱成功则返回,失败则重试(递归)
function doActionWithPass(i) {
return passOrFailed(i).catch(() => {
doActionWithPass(i);
})
}
// async实现异步函数的串行
async function doActionWithAsync() {
for(let i = 0; i < total; i++){
await doActionWithPass(i);
}
}
doActionWithAsync().then(() => console.log('all finished!'));
输出:
3.异步并行非阻塞
3.1 异步并行非阻塞: (ES6:Promise对象)
- 1.先用循环把所有的任务布置给小弟
- 2.小弟任务失败,则重来,成功则放到map里
- 3.当map的长度等于total,代表所有小弟完成任务
const total = 5; // 共5个小弟
let map = new Map(); // 记录完成任务的小弟
// 模拟小弟取钱的函数
function passOrFailed(i){
return new Promise((resolve, reject) => {
console.log(`=======模拟第${i}个小弟去取钱=======`)
// 异步操作
setTimeout(() => {
if (Math.random() < 0.7){
console.log(`${i}成功`);
resolve();
}else{
console.log(`${i}失败`);
reject();
}
}, Math.floor(Math.random()*1000)); //每个任务完成的时间不定
})
}
// 小弟取钱成功则放进map验收,失败则递归
function doActionsParallelly(i, resolve){
let p1 = passOrFailed(i);
p1.then(() => {
map.set(i, true)
if(map.size === total){
resolve();
}
}).catch(() => {
doActionsParallelly(i, resolve);
})
}
// 统一给小弟分发所有任务,让他们各自去执行
function doActionsWithPromise() {
return new Promise((resolve, reject) => {
for(let i = 0; i < total; i ++){
doActionsParallelly(i,resolve);
}
})
}
// 当所有都完成,输出All Finish!!!
doActionsWithPromise().then(() => {console.log('All Finish!!!')})
输出:
3.2 异步并行非阻塞: (ES6: Async-await)
const total = 5; // 共5个小弟
let map = new Map(); // 记录完成任务的小弟
// 模拟小弟取钱的函数
function passOrFailed(i){
return new Promise((resolve, reject) => {
console.log(`=======模拟第${i}个小弟去取钱=======`)
// 异步操作
setTimeout(() => {
if (Math.random() < 0.7){
console.log(`${i}成功`);
resolve();
}else{
console.log(`${i}失败`);
reject();
}
}, Math.floor(Math.random()*1000)); //每个任务完成的时间不定
})
}
// 小弟取钱只许成功,不许失败
function doAnActionWithPass(i) {
return passOrFailed(i).catch(() => {
doAnActionWithPass(i);
})
}
async function doActionWithAsync() {
for(let i = 0; i < total; i++){
let p = doAnActionWithPass(i);
console.log(p)
map.set(i, p);
}
for(let i = 0; i < total; i++){
await map.get(i);
}
}
doActionWithAsync().then(() => console.log('all finished!'))
.catch(() => console.log('some failed!'));
输出:
留下一个问题,为什么最后一个案例,allfinish不是最后输出呢?是代码里哪里写错了呢?欢迎在评论区解答~~