因为JavaScript是单线程运行的, 也就是说在同一时刻, JavaScript只能执行一个任务, 其他任务只能等待上个任务执行完毕再依次执行, 由此循环往复, 便引出事件循环的概念。
事件循环
事件循环理解比较容易, 如上文所说, JavaScript引擎在 “等待任务”,"执行任务"和"进入休眠状态等待更多任务"这几个状态之间转换的循环便是事件循环。
一般执行过程: 当有任务时, 从最先进入的任务开始执行, 如执行完没有其他任务,休眠直到出现任务,然后再从最先进入的任务开始执行。
注:事件循环中的事件并不是事件监听中的事件,这里的事件可以简单的理解为就是需要执行任务代码。
任务队列
在一次事件循环中可以有一个或多个任务队列, 一个任务队列便是一系列有序任务的集合,如下代码所示;
// 以一次性定时器为例
// 第一个任务队列
setTimeout(() => {
console.log(1)
new Promise(() => {}).then(res => {}) // then 的执行属于微任务
console.log(2)
document.querySelector('div').style.color = 'blue' // DOM渲染
})
// 第二个任务队列
setTimeout(() => {
//......
})
事件循环的关键步骤如下:
- 在循环中选择最先进入队列的任务,如示例代码中第一个任务队列中的
console.log(1)
; - 检查此队列中是否存在微任务,如果存在则不停地执行,直至清空微任务队列, 如示例代码中的
new Promise(() => {}).then(res => {})
; - 更新 render(DOM渲染), 如示例代码中
document.querySelector('div').style.color = 'blue'
; - 第一个任务队列执行完成后, 进入第二个任务队列… , 主线程重复执行上述步骤。
在上述循环的基础上需要了解几点:
- JS分为同步任务(如示例代码中的
console.log()
操作)和 异步任务(如示例代码中的new Promise(() => {}).then(res => {})
操作); - 同步任务都在主线程上执行, 形成一个执行栈;
- 异步任务会进入到宿主环境(浏览器便是宿主环境之一)管理的一个任务队列中, 只要异步任务有了运行结果,就在任务队列之中放置一个事件;
- 一旦执行栈中的所有同步任务执行完毕(如示例代码中的
console.log(1)与console.log(2)
执行完毕, 此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
宏任务
可以通俗的理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 如示例代码中一个任务队列便是一个宏任务。
常见的宏任务:
任务(代码) | 宏任务 | 环境 |
---|---|---|
script | 宏任务 | 浏览器 |
事件 | 宏任务 | 浏览器 |
网络请求(Ajax) | 宏任务 | 浏览器 |
setTimeout() 定时器 | 宏任务 | 浏览器 / Node |
fs.readFile() 读取文件 | 宏任务 | Node |
微任务
微任务可以理解为是宏任务中的一个部分,它的执行时机是在同步代码执行之后,下一个宏任务执行之前。如示例代码中new Promise(() => {}).then(res => {})
then 的执行就属于微任务。
运行机制
最后通过一个示例来对运行机制进行总结
// 第一个宏任务
<script>
// 同步任务1
console.log(1)
// 微任务1
new Promise((resolve, reject) => {
resolve(8)
}).then(res => {
console.log(res)
})
function abc() {
console.log(2)
}
// 同步任务2
abc()
// 第二个宏任务
setTimeout(() => {
// 同步任务4
console.log(3)
// 微任务3
new Promise((resolve, reject) => {
resolve(7)
}).then(res => {
console.log(res)
})
// 同步任务5
console.log(4)
}, 0)
// 微任务2
new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)
})
// 同步任务3
console.log(6)
// DOM渲染
document.querySelector('div').style.color = 'blue'
</script>
代码执行过程如下:
-
执行一个宏任务(执行栈中没有就从事件队列中获取)
-
代码从按照上至下的顺序开始执行, 获取到
script
标签(第一个宏任务), 开始执行;<script>...</script>
-
将第一个宏任务中的
同步任务
(同步任务1、同步任务2、同步任务3)在执行栈
中, 依次执行;一、执行同步任务 // 同步任务1 console.log(1) function abc() { console.log(2) } // 同步任务2 abc() // 同步任务3 console.log(6) // 控制台打印 1 2 6
-
-
执行过程中如果遇到
微任务
,就将它添加到微任务的任务队列
中-
将第一个宏任务中的微任务(微任务1、微任务2)添加到微任务队列, 等待同步任务执行完毕;
二、将微任务添加在微任务队列中 // 微任务队列 // 微任务1 new Promise((resolve, reject) => { resolve(8) }).then(res => { console.log(res) }) // 微任务2 new Promise((resolve, reject) => { resolve(5) }).then(res => { console.log(res) })
-
-
宏任务
执行完毕
后,立即执行当前微任务队列中的所有微任务(依次执行)-
执行完第一个宏任务中的同步任务后, 执行微任务队列中的微任务;
三、执行微任务 // 微任务1 new Promise((resolve, reject) => { resolve(8) }).then(res => { console.log(res) }) // 微任务2 new Promise((resolve, reject) => { resolve(5) }).then(res => { console.log(res) }) // 控制台打印 8 5
-
-
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
-
进行DOM 渲染
document.querySelector('div').style.color = 'blue'
-
-
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
-
开始执行第二个宏任务
// 第二个宏任务 setTimeout(() => { // 同步任务4 console.log(3) // 微任务3 new Promise((resolve, reject) => { resolve(7) }).then(res => { console.log(res) }) // 同步任务5 console.log(4) }, 0)
-
先执行同步任务(同步任务4、同步任务5), 将微任务添加至微任务队列
// 同步任务4 console.log(3) // 同步任务5 console.log(4) // 控制台打印 3 4
-
同步任务执行完成后, 执行微任务队列中的为任务
// 微任务3 new Promise((resolve, reject) => { resolve(7) }).then(res => { console.log(res) }) // 控制台打印 7
-
DOM渲染
-
进入下一个宏任务
-
如没有宏任务, 则进入休眠状态…
-