详解JS异步编程从回调地狱到Promise与async/await的原理与实践

详解JS异步编程从回调地狱到Promise与async/await的原理与实践

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

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

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

一、单线程和异步

1、单线程是什么

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

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

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

(3) JS 和 DOM 渲染共用同一个线程,因为 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 主要有以上三种状态, pending 、 fulfilled 和 rejected 。当返回一个 pending 状态的 promise 时,不会触发 then 和 catch 。当返回一个 fulfilled 状态时,会触发 then 回调函数。当返回一个 rejected 状态时,会触发 catch 回调函数。那在这几个状态之间,他们是怎么变化的呢?

1)演示1

先来看一段代码。

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

});

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

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

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

2)演示2

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

setTimeout(() => {

resolved();

});

});

console.log('p2', p2); //pending 一开始打印时

setTimeout(() => console.log('p2-setTimeout', p2)); //fulfilled

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

在这段代码中, 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

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

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

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

(2)状态的表现

pending 状态,不会触发 then 和 catch 。

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);

});

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

在这段代码中, 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);

})

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

在这段代码中, 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');

});

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

在这段代码中, 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);

});

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

在这段代码中, 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);

});

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

在这段代码中, 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');

});

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

在这段代码中, 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);

});

这道题打印的是 1 和 3 ,因为调用的是 promise 的 resolve 函数,所以后续不会触发 .catch 函数。

第二题:

Promise.resolve().then(() => {

console.log(1);

throw new Error('error');

}).catch(() => {

console.log(2);

}).then(() => {

console.log(3);

});

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

第三题:

Promise.resolve().then(() => {

console.log(1);

throw new Error('error');

}).catch(() => {

console.log(2);

}).catch(() => {

//这里是catch

console.log(3);

});

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

三、async/await

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

1、引例阐述

先用一个例子来展示 promise 和 async/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);

})();

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

2、async和await

背景:解决异步回调问题,防止深陷回调地狱 Callback hell ;

Promise :Promise then catch 是链式调用,但也是基于回调函数;

async/await :async/await 是同步语法,彻底消灭回调函数,是消灭异步回调的终极武器。

3、async/await和promise的关系

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

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

(3) await 相当于 promise 的 then 。

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

接下来我们来一一(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

});

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

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

第二条规则: await 相当于 promise 的 then 。

!(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

})();

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

大家可以看到, p1 调用 resolve 回调函数,所以此时 p1 属于 fulfilled 状态,之后 const data = await p1 中的await,相当于 promise 的 then ,又因为此时 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); //不打印

})();

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

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

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

!(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

}

})();

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

大家可以看到, 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 ,之后执行回调函数 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

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

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

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, promise 和 async/await 在我们日常的前端开发中还是蛮重要的,基本上写异步代码时候都会用到 async/await 来解决。啃了16个小时总结了event loop 和 promise 、async/await 问题,希望对大家有帮助。

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

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

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

相关推荐

慈悲──团结人心之力
mobile365官方网站立即加入

慈悲──团结人心之力

12-09 👁️ 6542
对抗寒体质的药有哪些
mobile365官方网站立即加入

对抗寒体质的药有哪些

10-18 👁️ 3588
拳皇97放大招按哪个按键
365官方入口-app下载

拳皇97放大招按哪个按键

01-22 👁️ 3187