同步和异步
想必大家都听说过JavaScript是单线程的,那么这门语言又怎么来的同步和异步的说法呢?这里就涉及到一个很重要的点,JavaScript虽然是单线程的,但浏览器有多个线程,对于一个单页面,不仅有线程负责处理JavaScript代码,还有负责网络请求的线程,负责监听用户操作的线程,这些东西处理好之后会进入任务队列(后文会详谈),以通知主线程,即执行JavaScript代码的那个线程,这里就体现了异步的特点。
JavaScript本身出于简单的考虑,并没有提供开启线程的api,所以说JavaScript是单线程的也没毛病,但切勿以为这就说明整个页面就只在走JavaScript这一条线程了,这是不现实的
宏任务和微任务
接着上文,所有任务分为同步和异步两种,同步的就是很快就能得到结果的主线程遇到同步任务时会立马执行,而不会拖延到之后,而异步的情况则比较复杂。
异步任务分为宏任务和微任务两种,微任务处理完毕后才会处理宏任务,而该异步任务被判定为宏还是微取决于官方的定义。常见分类如下:
任务 | 类型 |
---|---|
script(整体代码) | 宏任务 |
用户交互 | 宏任务 |
IO操作 | 宏任务 |
setTimeout | 宏任务 |
setInterval | 宏任务 |
Promise.then | 微任务 |
catch、finally | 微任务 |
任务队列
当主线程遇到异步任务时,后台会分配线程处理,并再处理完之后将其的回调函数放入任务队列。这就是任务队列的组成,注意到是异步任务的回调函数推入到任务队列的,而非异步任务本身。
EventLoop
有了上述概念,就能轻松理解EventLoop是怎么一回事了。
JavaScript引擎会不断轮询任务队列,将最先加入的宏任务取出,放入调用栈,然后开始执行其中代码,引擎会从上至下执行完所有同步代码,当遇到微任务时,会放到最后来执行,而遇到宏任务则会放到任务队列的最末尾等待执行。(这里只是简单的阐述了EventLoop的过程,具体实现会复杂一些)
参见下图:
Promise
我们知道,Promise的出现是为了拯救回调地狱这种情况。但其实在我看来其并未改变回调地狱的本质,大家都是发送Ajax请求,获得结果后,其回调函数会放在任务队列中等待执行。只不过之前我们是把所有之后的请求都写在第一个回调函数中,导致看起来像一个洋葱,层层嵌套,而Promise则把请求和回调函数分开来写,每一个then中可以再返回一个Promise,使之看起来像一条链子
//回调地狱
ajax(url, () => {
//业务代码
ajax(url1, () => {
//业务代码
ajax(url2, () => {
//业务代码
})
})
})
//Promise版本
ajax(url)
.then(res => {
//业务代码
})
.then(res => {
//业务代码
})
.then(res => {
//业务代码
});
async和await
await其实就是Promise的一些封装,使用await会比使用Promise看起来更加美观,就像同步代码一样,如下:
//async、await版本
async function foo(){
let res1 = await ajax1(url)
let res2 = await ajax2(res2)
return res2
}
await之前的代码可以理解为new Promise传入的代码,而之后的代码则是Promise.then中的回调。
看下面例子:
setTimeout(_=>{
console.log('10')
},0)
async function foo(){
console.log('2')
let msg = await new Promise((res,rej)=>{
console.log('3')
res('9')
console.log('4')
})
console.log('6')
await new Promise((resolve, reject) => {
console.log('7')
resolve('000')
console.log('8')
})
return msg
}
console.log('1')
foo().then(data => console.log(data)).catch(err=> console.log(err))
console.log('5')
正确的输出语序就是按数字大小来输出的。