解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!
Posted 星期一研究室
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决异步问题,教你如何写出优雅的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) 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 () 以上是关于解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!的主要内容,如果未能解决你的问题,请参考以下文章