我们平常在写 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 问题,希望对大家有帮助。
同时,里面可能有一些讲的不好或者不容易理解的地方也欢迎评论区评论或私信我交流~
关注公众号 星期一研究室 ,不定期分享学习干货~
如果这篇文章对你有用,记得点个赞加个关注哦~