啃下ES6的硬骨头-异步执行的那些事儿

1,107 阅读3分钟

最近被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不是最后输出呢?是代码里哪里写错了呢?欢迎在评论区解答~~