解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!

简介: 该文章教授了如何使用Promise和async/await来解决异步编程问题,从而避免回调地狱,使代码更加清晰和易于管理。

我们平常在写 js 中,经常只管程序能跑就行,但很少去深究它的原理。更别说还有一个同步和异步的问题了。因此,程序往往有时候出现莫名其妙的卡死或者有时候执行顺序达不到我们想要的结果时自己都不知道往哪里找错。下面的这篇文章中,将讲解同步和异步的问题,以及如何解决异步问题的promise、async/await方法。

在看本文之前,建议大家先对 event loop 有一个了解。了解event loop以及微任务宏任务的相关知识,这样到后面看本文的 async/await 的时候会更友好一些。

下面开始进行本文的讲解。

一、单线程和异步

1、单线程是什么

(1) JS 是单线程语言,只能同时做一件事情

  • 所谓单线程,就是只能同时做一件事情,多一件都不行,这就是单线程。

(2) 浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker

(3) JSDOM 渲染共用同一个线程,因为 JS 可修改 DOM 结构

  • JS 可以修改 DOM 结构,使得它们必须共用同一个线程,这间接算是一件迫不得已的事情。
  • 所以当 DOM 在渲染时 JS 必须停止执行,而 JS 执行过程 DOM 渲染也必须停止。

2、为什么需要异步

当程序遇到网络请求或定时任务等问题时,这个时候会有一个等待时间。

假设一个定时器设置10s,如果放在同步任务里,同步任务会阻塞代码执行,我们会等待10s后才能看到我们想要的结果。1个定时器的等待时间可能还好,如果这个时候是100个定时器呢?我们总不能等待着1000s的时间就为了看到我们想要的结果吧,这几乎不太现实。

那么这个时候就需要异步,通过异步来让程序不阻塞代码执行,灵活执行程序。

3、使用异步的场景

(1)异步的请求,如ajax图片加载

//ajax
console.log('start');
$.get('./data1.json', function (data1) {
   
    console.log(data1);
});
console.log('end');

(2)定时任务,如setTimeout、setInterval

//setTimeout
console.log(100);
setTimeout(fucntion(){
   
    console.log(200);           
}, 1000);
console.log(300);
//setInterval
console.log(100);
setInterval(fucntion(){
   
    console.log(200);           
}, 1000);
console.log(300);

二、promise

早期我们在解决异步问题的时候,基本上都是使用callback回调函数的形式 来调用的。形式如下:

//获取第一份数据
$.get(url1, (data1) => {
   
    console.log(data1);

    //获取第二份数据
    $.get(url2, (data2) => {
   
        console.log(data2);

        //获取第三份数据
        $.get(url3, (data3) => {
   
            console.log(data3);

            //还可以获取更多数据
        });
    });
});

从上述代码中可以看到,早期在调用数据的时候,都是一层套一层, callback 调用 callback ,仿佛深陷调用地狱一样,数据也被调用的非常乱七八糟的。所以,因为 callback 对开发如此不友好,也就有了后来的 promise 产生, promise 的出现解决了 callback hell 的问题。

用一段代码先来了解一下 Promise

function getData(url){
   
    return new Promise((resolve, reject) => {
   
        $.ajax({
   
            url,
            success(data){
   
                resolve(data);
            },
            error(err){
   
                reject(err);
            }
        });
    });
}

const url1 = '/data1.json';
const url2 = '/data2.json';
const url3 = './data3.json';
getData(url1).then(data1 => {
   
    console.log(data1);
    return getData(url2);
}).then(data2 => {
   
    console.log(data2);
    return getData(url3);
}).then(data3 => {
   
    console.log(data3);
}).catch(err => console.error(err));

大家可以看到,运用了 promise 之后,代码不再是一层套一层,而是通过 .then 的方式来对数据进行一个获取,这在写法上就已经美观了不少。那 promise 究竟是什么呢?接下来开始进行讲解。

1、promise的三种状态

  • pending :等待状态,即在过程中,还没有结果。比如正在网络请求,或定时器没有到时间。

  • fulfilled :满足状态,即事件已经解决了,并且成功了;当我们主动回调了 fulfilled 时,就处于该状态,并且会回调 then 函数。

  • rejected :拒绝状态,即事件已经被拒绝了,也就是失败了;当我们主动回调了 reject 时,就处于该状态,并且会回调 catch 函数。

总结:

  • 只会出现pending → fulfilled,或者pending → rejected 状态,即要么成功要么失败;
  • 不管是成功的状态还是失败的状态,结果都不可逆。

2、三种状态的表现和变化

(1)状态的变化

promise 主要有以上三种状态, pendingfulfilledrejected 。当返回一个 pending 状态的 promise 时,不会触发 thencatch 。当返回一个 fulfilled 状态时,会触发 then 回调函数。当返回一个 rejected 状态时,会触发 catch 回调函数。那在这几个状态之间,他们是怎么变化的呢?

1)演示1

先来看一段代码。

const p1 = new Promise((resolved, rejected) => {
   

});

console.log('p1', p1); //pending

在以上的这段代码中,控制台打印结果如下。

状态变化演示1

在这段代码中, p1 函数里面没有内容可以执行,所以一直在等待状态,因此是 pending

2)演示2

const p2 = new Promise((resolved, rejected) => {
   
    setTimeout(() => {
   
        resolved();
    });
});

console.log('p2', p2); //pending 一开始打印时
setTimeout(() => console.log('p2-setTimeout', p2)); //fulfilled

在以上的这段代码中,控制台打印结果如下。

状态变化演示2

在这段代码中, p2 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,才会触发到 resolved 函数,触发后返回一个 fulfilled 状态 promise

3)演示3

const p3 = new Promise((resolved, rejected) => {
   
    setTimeout(() => {
   
        rejected();
    });
});

console.log('p3', p3);
setTimeout(() => console.log('p3-setTimeout', p3)); //rejected

在以上的这段代码中,控制台打印结果如下。

状态变化演示3

在这段代码中, p3 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,同样地,会触发到 rejected 函数,触发后返回一个 rejected 状态的 promise

看完 promise 状态的变化后,相信大家对 promise 的三种状态分别在什么时候触发会有一定的了解。那么我们接下来继续看 promise 状态的表现。

(2)状态的表现

  • pending 状态,不会触发 thencatch
  • fulfilled 状态,会触发后续的 then 回调函数。
  • rejected 状态,会触发后续的 catch 回调函数。

我们来演示一下。

1)演示1

const p1 = Promise.resolve(100); //fulfilled
console.log('p1', p1);
p1.then(data => {
   
    console.log('data', data);
}).catch(err => {
   
    console.error('err', err);
});

在以上的这段代码中,控制台打印结果如下。

状态的表现演示1

在这段代码中, p1 调用 promise 中的 resolved 回调函数,此时执行时, p1 属于 fulfilled 状态, fulfilled 状态下,只会触发 .then 回调函数,不会触发 .catch ,所以最终打印出 data 100

2)演示2

const p2 = Promise.reject('404'); //rejected
console.log('p2', p2);
p2.then(data => {
   
    console.log('data2', data);
}).catch(err => {
   
    console.log('err2', err);
})

在以上的这段代码中,控制台打印结果如下。

状态的表现演示2

在这段代码中, p2 调用 promise 中的 reject 回调函数,此时执行时, p1 属于 reject 状态, reject 状态下,只会触发 .catch 回调函数,不会触发 .then ,所以最终打印出 err2 404

对三种状态有了基础了解之后,我们来深入了解 .then.catch 对状态的影响。

3、then和catch对状态的影响(重要)

  • then 正常返回 fulfilled ,里面有报错则返回 rejected
  • catch 正常返回 fulfilled ,里面有报错则返回 rejected

我们先来看第一条规则: then 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1

const p1 = Promise.resolve().then(() => {
   
    return 100;
})
console.log('p1', p1); //fulfilled状态,会触发后续的.then回调
p1.then(() => {
   
    console.log('123');
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示1

在这段代码中, p1 调用 promise 中的 resolve 回调函数,此时执行时, p1 正常返回 fulfilled , 不报错,所以最终打印出 123

2)演示2

const p2 = Promise.resolve().then(() => {
   
    throw new Error('then error');
});
console.log('p2', p2); //rejected状态,触发后续.catch回调
p2.then(() => {
   
    console.log('456');
}).catch(err => {
   
    console.error('err404', err);
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示2

在这段代码中, p2 调用 promise 中的 resolve 回调函数,此时执行时, p2 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 所以最终打印出 err404 Error: then error 的结果。

我们再来看第二条规则catch 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1(需特别谨慎! !)

const p3 = Promise.reject('my error').catch(err => {
   
    console.error(err);
});
console.log('p3', p3); //fulfilled状态,注意!触发后续.then回调
p3.then(() => {
   
    console.log(100);
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示3

在这段代码中, p3 调用 promise 中的 rejected 回调函数,此时执行时, p3 在执行过程中,正常返回了一个 Error这个点需要特别谨慎!!这看起来似乎有点违背常理,但对于 promise 来说,不管时调用 resolved 还是 rejected ,只要是正常返回而没有抛出异常,都是返回 fulfilled 状态。所以,最终 p3 的状态是 fulfilled 状态,且因为是 fulfilled 状态,之后还可以继续调用 .then 函数。

2)演示2

const p4 = Promise.reject('my error').catch(err => {
   
    throw new Error('catch err');
});
console.log('p4', p4); //rejected状态,触发.catch回调函数
p4.then(() => {
   
    console.log(200);
}).catch(() => {
   
    console.log('some err');
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示4

在这段代码中, p4 依然调用 promise 中的 reject 回调函数,此时执行时, p4 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 此时 p4 的状态为 rejected ,之后触发后续的 .catch 回调函数。所以最终打印出 some err 的结果。

4、then和catch的链式调用(常考)

学习完以上知识后,我们通过几道题来再总结回顾一下。

第一题:

Promise.resolve().then(() => {
   
    console.log(1);
}).catch(() => {
   
    console.log(2);
}).then(() => {
   
    console.log(3);
});

这道题打印的是 13 ,因为调用的是 promiseresolve 函数,所以后续不会触发 .catch 函数。

第二题:

Promise.resolve().then(() => {
   
    console.log(1);
    throw new Error('error');
}).catch(() => {
   
    console.log(2);
}).then(() => {
   
    console.log(3);
});

这道题打印的是 12 ,虽然调用的是 promiseresolve 函数,但是中间抛出了一个异常,所以此时 promise 变为 rejected 状态,所以后续不会触发 .then 函数。

第三题:

Promise.resolve().then(() => {
   
    console.log(1);
    throw new Error('error');
}).catch(() => {
   
    console.log(2);
}).catch(() => {
     //这里是catch
    console.log(3);
});

这道题打印的是 123 ,跟第二题一样,中间抛出了一个异常,所以此时 promise 变为 rejected 状态,所以后续只触发 .catch 函数。

三、async/await

现代 js 的异步开发,基本上被 asyncawait 给承包和普及了。虽然说 promise 中的 .then.catch 已经很简洁了,但是 async 更简洁,它可以通过写同步代码来执行异步的效果。如此神奇的 asyncawait 究竟是什么呢?让我们一起来一探究竟吧!

1、引例阐述

先用一个例子来展示 promiseasync/await 的区别。假设我们现在要用异步来实现加载图片。

(1) 如果用 promise.then.catch 实现时,代码如下:

function loadImg(src){
   
    const picture = new Promise(
        (resolve, reject) => {
   
            const img = document.createElement('img');
            img.onload = () => {
   
                resolve(img);
            }
            img.onerror = () => {
   
                const err = new Error(`图片加载失败 ${
     src}`);
                reject(err);
            }
            img.src = src;
        }
    )
    return picture;
}

const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58';
const url2 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=614367910,2483275034&fm=58';

loadImg(url1).then(img1 => {
   
    console.log(img1.width);
    return img1; //普通对象
}).then(img1 => {
   
    console.log(img1.height);
    return loadImg(url2); //promise 实例
}).then(img2 => {
   
    console.log(img2.width);
    return img2;
}).then(img2 => {
   
    console.log(img2.height);
}).catch(ex => console.error(ex));

(2) 如果用 async 实现时,代码如下:

function loadImg(src){
   
    const picture = new Promise(
        (resolve, reject) => {
   
            const img = document.createElement('img');
            img.onload = () => {
   
                resolve(img);
            }
            img.onerror = () => {
   
                const err = new Error(`图片加载失败 ${
     src}`);
                reject(err);
            }
            img.src = src;
        }
    )
    return picture;
}

const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58';
const url2 = 'https://dss2.bdstatic.com/8_V1bjqh_Q23odCf/pacific/1990552278.jpg';


!(async function () {
   
    // img1
    const img1 = await loadImg(url1);
    console.log(img1.width, img1.height);

    // img2
    const img2 = await loadImg(url2);
    console.log(img2.width, img2.height);
})();

大家可以看到,如果用第二种方式的话,代码要优雅许多。且最关键的是,通过 asyncawait ,用同步代码就可以实现异步的功能。接下来我们开始来了解 asyncawait

2、async和await

  • 背景:解决异步回调问题,防止深陷回调地狱 Callback hell
  • PromisePromise then catch 是链式调用,但也是基于回调函数;
  • async/awaitasync/await同步语法,彻底消灭回调函数,是消灭异步回调的终极武器。

3、async/await和promise的关系

(1) async/awaitpromise 并不互斥,两者相辅相成。

(2) 执行 async 函数,返回的是 promise 对象。

(3) await 相当于 promisethen

(4) try…catch 可以捕获异常,代替了 promisecatch

接下来我们来一一(2)(3)(4)演示这几条规则。

第一条规则: 执行 async 函数,返回的是 promise 对象。

async function fn1(){
   
    return 100; //相当于return promise.resolve(100);
}

const res1 = fn1(); //执行async函数,返回的是一个Promise对象
console.log('res1', res1); //promise对象
res1.then(data => {
   
    console.log('data', data); //100
});

在以上的这段代码中,控制台打印结果如下。

async/await第一条规则

大家可以看到,第一个 res1 返回的是一个 promise 对象,且此时 promise 对象的状态是 fulfilled 状态,所以可以调用后续的 .then 并且打印出 data 100

第二条规则: await 相当于 promisethen

!(async function (){
   
    const p1 = Promise.resolve(300);
    const data = await p1; //await 相当于 promise的then
    console.log('data', data); //data 300
})();

!(async function () {
   
    const data1 = await 400; //await Promise.resolve(400)
    console.log('data1', data1); //data1 400
})();

在以上的这段代码中,控制台打印结果如下。

async/await第二条规则

大家可以看到, p1 调用 resolve 回调函数,所以此时 p1 属于 fulfilled 状态,之后 const data = await p1 中的await,相当于 promisethen ,又因为此时 p1 属于 fulfilled 状态,所以可以对 .then 进行调用,于是输出 data 300 。同理在第二段代码中, await 400 时, 400 即表示 Promise.resolved(400) ,因此属于 fulfilled 状态,随后调用 .then ,打印出 data1 400 结果。

再来看一段代码:

!(async function (){
   
    const p2 = Promise.reject('err1');
    const res = await p4; //await 相当于 promise的then
    console.log('res', res); //不打印
})();

在以上的这段代码中,控制台打印结果如下。

async/await第三条规则

大家可以看到, p2 调用 reject 回调函数,所以此时 p2 属于 reject 状态。但因为await是触发 promise 中的 .then ,所以此时 res 不会被触发,于是后续不会对await进行操作,控制台也就不对 console.log('res', res); 进行打印。

第三条规则: try…catch 可以捕获异常,代替了 promisecatch

!(async function () {
   
    const p3 = Promise.reject('err1'); //rejected 状态
    try{
   
        const res = await p3;
        console.log(res);
    }catch(ex){
   
        console.error(ex); //try…catch 相当于 promise的catch
    }
})();

在以上的这段代码中,控制台打印结果如下。

async/await第三条规则

大家可以看到, p3 调用 reject 回调函数,所以此时 p3 属于 rejected 状态,因此它不会执行 try 的内容,而是去执行 catch 的内容, try…catch 中的 catch 就相当于 promise 中的 catch ,且此时 p3 属于 rejected 状态,因此执行 catch ,浏览器捕获到异常,报出错误。

4、异步的本质

从上面的分析中,不管是 promise 还是 async/await ,都是解决异步问题。但是呢,异步的本质还是解决同步的问题,所以,异步的本质是:

  • async/await 是消灭异步回调的终极武器;
  • JS 是单线程的,需要有异步,需要基于 event loop
  • async/await 是一个语法糖,但是这颗糖非常好用!!

我们来看两道 async/await 的顺序问题,回顾 async/await

第一题:

async function async1(){
   
    console.log('async start'); // 2
    await async2();
    //await 的后面,都可以看做是callback里面的内容,即异步。
    //类似event loop
    //Promise.resolve().then(() => console.log('async1 end'))
    console.log('async1 end'); // 5
}

async function async2(){
   
    console.log('async2'); //3
}

console.log('script start');  // 1
async1();
console.log('script end'); // 4

在以上的这段代码中,控制台打印结果如下。

异步的本质1

从上面这段代码中可以看到,先执行同步代码 1 ,之后执行回调函数 async1() ,在回调函数 async1() 当中,先执行同步代码 2 ,之后遇到 await ,值得注意的是, await 的后面,都可以看作是 callback 里面的内容,即异步内容,所以,先执行 await 中对应的 async2() 里面的内容,之后把 await 后面所有的内容放置到异步当中。继续执行 4 ,等到 4 执行完时,整个同步代码已经执行完,最后,再去执行异步的代码,最终输出 5 的内容。

同样的方式来来分析第二题。

第二题:

async function async1(){
   
    console.log('async1 start'); //2
    await async2();

    // 下面三行都是异步回调,callback的内容
    console.log('async1 end'); //5
    await async3();
    // 下面一行是回调的内容,相当于异步回调里面再嵌套一个异步回调。
    console.log('async1 end 2'); //7
}

async function async2(){
   
    console.log('async2'); //3
}

async function async3(){
   
    console.log('async3'); //6
}

console.log('script start'); //1
async1();
console.log('script end'); //4

在以上的这段代码中,控制台打印结果如下。

异步的本质2

这里就不再进行分析啦!大家可以根据第一个案例的步骤进行分析。

5、场景题

最后的最后,我们再来做两道题回顾我们刚刚讲过的 async/await 知识点。

(1)async/await语法

async function fn(){
   
    return 100;
}
(async function(){
   
    const a = fn(); //?? Promise
    const b = await fn(); //?? 100
})();
(async function(){
   
    console.log('start');
    const a = await 100;
    console.log('a', a);
    const b = await Promise.resolve(200);
    console.log('b', b);
    const c = await Promise.reject(300); //出错了,再往后都不会执行了
    console.log('c', c);
    console.log('end');
})(); //执行完毕,打印出哪些内容?
//start
//100
//200

(2)async/await的顺序问题

async function async1(){
   
    console.log('async1 start'); // 2
    await async2();
    //await后面的都作为回调内容 —— 微任务
    console.log('async1 end'); // 6
}

async function async2(){
   
    console.log('async2'); // 3
}

console.log('script start'); // 1

setTimeout(function(){
    //宏任务 —— setTimeout
    console.log('setTimeout'); // 8
}, 0);

//遇到函数,立马去执行函数
async1();

//初始化promise时,传入的函数会立刻被执行
new Promise(function(resolve){
    //promise —— 微任务
    console.log('promise1'); // 4
    resolve();
}).then(function(){
    //微任务
    console.log('promise2'); // 7
});

console.log('script end'); // 5

//同步代码执行完毕(event loop —— call stack 被清空)
//执行微任务
//(尝试触发 DOM 渲染)
// 触发event loop,执行宏任务

这里就不再进行一一解析啦!大家可以前面知识点的学习总计再回顾理解。

四、写在最后

关于 js 的异步问题以及异步的解决方案问题就讲到这里啦!u1s1, promiseasync/await 在我们日常的前端开发中还是蛮重要的,基本上写异步代码时候都会用到 async/await 来解决。啃了16个小时总结了event loop 和 promise 、async/await 问题,希望对大家有帮助。

同时,里面可能有一些讲的不好或者不容易理解的地方也欢迎评论区评论或私信我交流~

关注公众号 星期一研究室 ,不定期分享学习干货~

如果这篇文章对你有用,记得点个赞加个关注哦~

相关文章
|
6天前
|
存储 前端开发
除了 Promise.all(),还有哪些方法可以处理异步并发操作?
在上述示例中,`concurrentPromises` 函数接受一个Promise数组和最大并发数作为参数,通过手动控制并发执行的Promise数量,实现了对异步操作的并发控制,并在所有Promise完成后返回结果数组。
|
6天前
|
前端开发 JavaScript
如何使用 Promise 处理异步并发操作?
通过使用 `Promise.all()` 和 `Promise.race()` 方法,可以灵活地处理各种异步并发操作,根据不同的业务需求选择合适的方法来提高代码的性能和效率,同时也使异步代码的逻辑更加清晰和易于维护。
|
3天前
|
前端开发 数据处理
如何使用 Promise.all() 处理异步并发操作?
使用 `Promise.all()` 可以方便地处理多个异步并发操作,提高代码的执行效率和可读性,同时通过统一的 `.catch()` 方法能够有效地处理异步操作中的错误,确保程序的稳定性。
|
12天前
|
前端开发 JavaScript 开发者
Async 和 Await 是基于 Promise 实现
【10月更文挑战第30天】Async和Await是基于Promise实现的语法糖,它们通过简洁的语法形式,借助Promise的异步处理机制,为JavaScript开发者提供了一种更优雅、更易于理解和维护的异步编程方式。
20 1
|
7天前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
|
6天前
|
前端开发 JavaScript 开发者
用 Promise 处理异步操作的优势是什么?
综上所述,使用Promise处理异步操作能够有效地解决传统回调函数带来的诸多问题,提高代码的质量、可读性、可维护性和可扩展性,是JavaScript中进行异步编程的重要工具和技术。
|
12天前
|
JSON 前端开发 JavaScript
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
19 1
|
1月前
|
前端开发 JavaScript UED
深入了解JavaScript异步编程:回调、Promise与async/await
【10月更文挑战第11天】深入了解JavaScript异步编程:回调、Promise与async/await
11 0
|
6月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
65 1
|
6月前
|
前端开发 JavaScript
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
在JavaScript中,什么是promise、怎么使用promise、怎么手写promise
99 4