1 js事件循环机制简单介绍
JavaScript 是单线程的,处理耗时任务时会阻塞程序执行。为解决这一问题,引入了异步任务机制。同步任务由 JavaScript 引擎直接处理,而异步任务则由宿主环境(如 Node.js、浏览器等多线程环境)执行。同步任务会立即加入执行栈并等待结果,而异步任务则在适当时机(例如定时器结束或点击事件发生时)被放入任务队列。当执行栈中的任务完成后,事件循环会检查任务队列中是否有待处理的任务,并将其逐一加入执行栈,循环往复,这便构成了事件循环机制。
1.1 宏任务与微任务队列
异步任务队列又被分为了两种不同的类型队列,分别是宏任务(Macrotasks)和微任务(Microtask)任务队列。
宏任务
宏任务是较大粒度的任务,通常对应于整体事件或动作。每个事件循环周期(Event Loop Cycle)中,事件循环会从宏任务队列中取出一个任务并执行(其实我们顶层的整个script也是一个宏任务)。
常见的宏任务类型
- script代码块(我们的脚本也是一个宏任务)
- setTimeout 和 setInterval:用于设置定时器,延迟执行代码。
- I/O 操作:如文件读取、网络请求等(在Node.js中)。
- UI 渲染事件:如 resize、scroll 等浏览器事件。
- 用户交互事件:如 click、keydown 等。
微任务
微任务是较小粒度的任务,通常用于在当前宏任务执行完毕后、下一个宏任务开始前执行一些需要尽快完成的操作。微任务队列的优先级高于宏任务队列。
常见的微任务类型
- Promise 的回调:如 then、catch、finally。
- MutationObserver:用于监测DOM变化。
- process.nextTick(Node.js特有):用于在当前操作完成后立即执行。
- async 和 await 特别是await后面的代码可以看作callback放入微任务队列
事件循环机制中的执行顺序
- 从宏任务队列中取出第一个宏任务并执行。
- 执行所有微任务队列中的任务,直到微任务队列为空。
- 进行渲染(如果需要)。
- 重复以上步骤,进入下一个事件循环周期。
1.2 事件循环机制举例说明
下面看一下示例代码1:
// 示例代码01
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0)
console.log('script end');
输出结果如下所示:
script start
script end
setTimeout
- 首先执行整个script,console.log是同步的,直接输出script start
- setTimeout是新的宏任务,放入宏任务队列,继续执行余下script代码
- console.log是同步的,直接输出script end
- 执行栈为空,微任务队列为空,取出宏任务的回调函数进行执行
- 输出setTimeout
下面看一下示例代码2,我们将引入微任务:
// 示例代码02
console.log('script start')
const p = new Promise((resolve)=>{
console.log('promise executor')
resolve(2)
console.log('promise executor end')
})
p.then((res) => {
console.log(res)
})
setTimeout(() => {
console.log('setTimeout')
})
console.log('script end')
输出结果如下所示:
script start
promise executor
promise executor end
script end
2
setTimeout
- 首先输出同步内容,script start,在创建Promise对象时传入的回调函数也是同步对象,会立即执行,输出promise excutor promise executor end。
- .then 和 .catch都是异步的微任务,将其放入微任务队列。
- setTimeout是宏任务队列,放入宏任务队列。
- 最后将console.log放入执行栈执行,输出script end,当前宏任务执行完毕,查看微任务队列。
- 取出微任务队列的then函数的回调函数,执行输出 2 ,微任务队列执行完毕,取出下一个宏任务执行。
- 执行宏任务队列中的setTimeout输出setTimeout
下面看最复杂的示例代码3:
// 示例代码03
async function async1() {
console.log('async1 start')
new Promise((resolve) => {
resolve('promise1')
setTimeout(() => {
resolve('setTimeout 2')
console.log('setTimeout 2')
}, 0)
new Promise((resolve) => {
setTimeout(() => {
resolve('setTimeout 3')
console.log('setTimeout 3')
}, 0)
}).then((res) => {
console.log(res)
})
}).then((res) => {
console.log(res)
})
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(()=>{
console.log("setTimeout 1")
}, 0)
async1()
Event loop 1
- 执行script,首先console.log放入执行栈输出 script start
- 宏任务setTimeout放入宏任务队列, 遇到异步方程,继续执行
- 执行异步函数async 1,首先输出async1 start进入后执行第一个Promise的回调函数,将setTimeout放如宏任务队列。
- 进入第二个嵌套的Promise的回调函数,添加setTimeout[setTimeout 3]
- 添加第二个Promise的then回调函数到微任务队列
- 碰到await 直接执行右侧函数,console.log放入执行栈输出 async2
- await之后的代码被视为then回调中的函数,放入微任务队列。
微任务队列:then(pending) then(promise 1) await(...async1 end)
宏任务队列:setTimeout[setTimeout 1] setTimeout[setTimeout 2] setTimeout[setTimeout 3]
Event loop 2
- 优先执行微任务,第一个微任务处于pending状态跳过,执行第一个promise的回调,输出promise1
- 执行await后的余下代码,输出async1 end,微任务执行完毕,执行下一个宏任务
- 取出setTimeout[setTimeout 1],输出setTimeout 1
- 宏任务结束,进入下一个事件循环
微任务队列:then(promise 2 pending)
宏任务队列:setTimeout[setTimeout 2] setTimeout[setTimeout 3]
Event loop 3
- 微任务队列中的then回调依然处于pending,等待resolve触发执行,取出setTimeout [setTimeout 2] 执行(注意多次调用resolve是无效的),输出setTimeout 2
- 宏任务执行完毕,该循环结束。
微任务队列:then(promise 2 pending)
宏任务队列:setTimeout[setTimeout 3]
Event loop 4
- 微任务队列中的then回调依然处于pending,等待resolve触发执行,取出setTimeout[setTimeout 3]的回调函数执行。
- resolve("setTimeout")将会触发then的callback的执行,输出setTimeout 3
- console.log输出setTimeout 3
- 任务队列结束,代码执行完毕
微任务队列:empty
宏任务队列:empty
最终输出结果
- script start
- async1 start
- async2
- promise1
- async1 end
- setTimeout 1
- setTimeout 2
- setTimeout 3
- setTimeout 3