前言
JavaScript从诞生起就是一门单线程的脚本语言,单线程就意味着JavaScript代码在执行的时候,都只会有一个主线程去执行所有的任务。单线程的一个弊端在于,当线程中存在一个耗时非常长的任务时,会容易出现代码阻塞,从而导致页面出现“假死”状态。为了解决这一问题,事件循环机制(Event Loop)就诞生了。
一、同步任务和异步任务
JavaScript中所有的任务被分为同步任务和异步任务。
-
同步任务:立即执行的任务
-
异步任务:不会立即执行的任务
-
常见的异步任务有setTimeout、Promise.then()、异步的ajax请求等。
二、执行栈和事件队列
1、 执行栈
栈是一种数据结构,遵循先进后出的规则。js中的执行栈就具有这样的结构。当代码第一次执行时,js引擎会解析这段代码并将其中的同步任务压到执行栈中,任务完成后被弹出执行栈,继续执行下一个任务。
举个例子:
function A(){
B()
}
function B(){
C()
}
function C(){}
A()
存在A、B、C 3个方法,A调用B,B调用C。
- 代码执行过程中,会先将A压入栈底,由于A中调用了B,所以此时A不会被弹出栈
- 接着将B压入栈,此时A在栈底,B在栈顶,由于B中还调用了C,所以B还是没有执行完成,不会被弹出栈
- 接着将C压入栈,此时,栈中的顺序是 栈底A -> B -> C栈顶
- C中没有调用其他的方法,故C执行完后,执行栈会遵循先进后出的原则依次弹出C、B、A
2、 事件队列(Task Queue)
队列也是一种数据结构,遵循先进先出的规则。上面我们讲的都是关于同步任务是如何执行的,那异步任务执行后会如何呢?
js在遇到异步任务时,并不会一直等待其返回结果,而是将该事件挂起,继续执行主线程中的任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的队列中去,这个队列就称之为事件队列(Task Queue)。
被放在事件队列中的异步任务不会立即的执行其回调,而是等待当前主线程中所有的同步任务都完成,执行栈为空的时候,主线程才会去事件队列读取是否有可执行的异步任务。如果有,会取出该任务并压入执行栈中执行其中的同步代码…如此反复,就形成了无限的循环,直到所有任务都执行完成。这个循环的过程我们就称之为“事件循环(Event Loop)”。
同样,我们举个例子:
console.log('start')
Promise.resolve().then(() => {
setTimeout(() => {
console.log('timeout')
}, 0)
console.log('promise')
})
console.log('end')
- 任务开始执行,将同步任务console.log(‘start’) 压入执行栈,执行结束后弹出栈
- a是异步任务,将Promise callback加入到事件队列中
- 将同步任务console.log(‘end’)压入执行栈,执行结束后弹出栈
- 此时主线程上的同步任务执行完成,执行栈为闲置状态,主线程读取事件队列,取出Promise任务并压入执行栈
- setTimeout为异步任务,加入到事件队列中,继续执行console.log(‘promise’),执行完成,弹出栈
- 主线程执行完成,继续读取事件队列,取出setTimeout并压入执行栈执行
- 任务全部执行完成,结束。最终打印结果为start -> end -> promise -> timeout
三、宏任务和微任务
以上描述的事件循环是宏观上的表述,事实上,js的异步任务之间并不相同,它们执行顺序的优先级也不一样。据此,异步任务中又被划分为宏任务(macro task)和微任务(micro task)。微任务的优先级高于宏任务。
宏任务包括:
script(整体代码)、setTimeout、setInterval、setImmediate等。
微任务包括:
Promise、Mutation Observer、async/await等。
前面我们讲到,异步任务会被放到事件队列中。然而,根据任务类型的不同,异步任务又会被分到对应的宏任务队列或微任务队列中。在当前的执行栈为空时,主线程会优先查找微任务队列是否有事件存在,如果有,则取出事件并压入执行栈执行,如果没有,执行宏任务队列的任务…如此反复…进入循环。
至此,js的事件循环就讲完了,最后,我们来看一段比较复杂的代码,进一步体会一下事件循环机制。
async function a() {
console.log('a')
await b()
console.log('async-after-b')
}
function b() {
console.log('b')
}
console.log('script-start')
a()
const promise = new Promise(resolve => {
console.log('promise')
resolve()
}).then(() => {
console.log('promise-cb1')
setTimeout(() => {
console.log('timeout1')
}, 0)
}).then(() => {
console.log('promise-cb2')
})
setTimeout(async () => {
console.log('timeout2')
await b()
console.log('timeout-after-b')
}, 0)
console.log('script-end')
打印结果我就不贴出来了,具体的执行过程我就不做详细的讲解,有兴趣的同学可以细细品一下!