Description
事件循环(event loop)是什么?
JavaScript作为单线程脚本语言,同一时刻只能做一件事情,所有任务都需要排队,前一个任务结束,才会执行后一个任务。想一想,如果中间有一个耗时长的任务(异步任务),例如:ajax
从服务器获取数据,难道我们要等待数据返回了才继续完成后面的任务吗?这显然不合理。
所以,事件循环就是帮我们解决这个问题的,它规定了如何来处理这些异步任务(ajax
、setTimeout
等)。
事件循环的工作方式大概是这样:
进入程序,主线程从任务队列中取读第一个任务(script
包裹的代码,我们可以认为它本身就是一个全局函数)加入执行栈(stack
)执行,如果遇到异步任务并不会立即执行其中的代码,而是交给WebAPIs
去处理,然后继续执行后面的代码,WebAPIs
会在适当的时机将这些任务加入到任务队列(callback queue
)中,执行栈中的代码执行完毕,就会取读任务队列中的任务,并加入执行栈执行,如此往复。
引用一张经典的图片:
以上图片中,异步任务有结果了就会将它所定义的回调函数加入到任务队列(callback queue
)中,执行栈清空后就会取读任务队列。
看看以下代码:
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)
// 1
// 3
// 2(1s后)
以上代码中,当JavaScript引擎遇到setTimeout
,会把它的回调函数和毫秒值交给WebAPIs
,主线程继续执行后面的代码,所以先打印了1
和3
,1s之后WebAPIs
中的回调函数会被加入到任务队列中,由于主线程现在是空闲状态,所以会立即取读任务队列并执行打印出2。
看看另一个例子:
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then( res => {
console.log(3)
})
console.log(4)
// 1 4 3 2
以上代码中,setTimeout
和Promise
都是异步任务,但Promise
先执行了,而不是setTimeout
?
原来在任务队列中还分为了:
微任务队列(Promise.then
)
以及宏任务队列(setTimeout
)
微任务属于本次任务的附属任务,本次任务结束后会取读微任务队里的所有任务并依次执行,然后开始下一个宏任务,也就是宏任务队列中的第一个任务,结束后继续查看微任务队列,能看出来事件循环大概是这样进行的:
宏任务 -> 微任务(全部) -> 宏任务 -> 微任务(全部) -> ···
所以,在上面的例子中,script
本身就是一个宏任务,执行完毕就会取读微任务队列(Promise.then
),然后是下一个宏任务(setTimeout
)。换句话说,微任务是被添加到了本次任务的末尾,而宏任务添加到了下次事件循环的开头,本次任务产生的微任务永远在宏任务之前执行。
常见的宏任务(macrotask)和微任务(microtask):
宏任务:
- script整体代码
- setTimeout
- setInterval
- I/O 操作
- UI交互事件
微任务:
- Promise.then
- MutaionObserver
一次完整的事件循环
看看另一个例子:
const div = document.querySelector(".box")
div.style.backgroundColor = "red"
Promise.resolve().then( res => {
div.style.backgroundColor = "blue"
})
以上代码中,通过观察发现,元素.box
的背景色自始至终都是蓝色
const div = document.querySelector(".box")
div.style.backgroundColor = "red"
setTimeout(() => {
div.style.backgroundColor = "blue"
}, 0)
以上代码中,元素.box
的背景色会先变成红色,然后立马变成蓝色
两段代码反映一个问题,就是关于UI重新渲染的时机,是在一次事件循环的末尾进行的,也就是微任务之后会进行UI的重新渲染。
所以完整的事件循环应该是这样:
宏任务 -> 微任务(全部) -> UI渲染 -> 宏任务 -> 微任务(全部) -> UI渲染 ···