JS 的事件循环机制
调用栈、宏任务、微任务和事件循环协同工作流程:同步任务首先执行,接着执行微任务队列中的所有任务,最后执行宏任务队列中的任务。通过这种机制,JavaScript 能够有效地处理异步任务,并确保它们在正确的时机执行。
调用栈(Call Stack)
介绍
调用栈是一个后进先出(LIFO, Last-In-First-Out)的栈结构,用于管理函数调用和执行上下文。当 JavaScript 引擎执行代码时,会创建执行上下文并将其推入调用栈。函数调用完成后,对应的执行上下文会从调用栈中弹出。
执行上下文
执行上下文是 JavaScript 执行代码时的环境,包含了代码执行所需的所有信息。每个执行上下文都有三个主要部分:
- 变量对象(Variable Object,VO):包含了变量、函数声明和函数参数。
- 作用域链(Scope Chain):当前执行上下文及其父级执行上下文的变量对象的列表,用于标识标识符的可见性和可访问性。(当在执行上下文中访问一个变量时,JavaScript 引擎会按照作用域链进行查找。)
- this 绑定:指向执行上下文中的 this 对象。
全局执行上下文
全局执行上下文是 JavaScript 代码启动时创建的第一个执行上下文,它包含全局作用域。全局执行上下文有以下特点:
- 变量对象:在全局执行上下文中,变量对象是全局对象(如
window
对象在浏览器中)。 - 作用域链:全局执行上下文的作用域链仅包含全局变量对象。
- this 绑定:在全局执行上下文中,
this
绑定到全局对象(如浏览器中的window
对象)。
函数执行上下文
函数执行上下文:每当函数被调用时,JavaScript 会创建一个新的执行上下文,该执行上下文有其自己的变量对象、作用域链和 this 绑定。函数执行上下文的作用域链是以其自身的变量对象为起点,然后是父级执行上下文的变量对象,最终指向全局执行上下文的变量对象。
宏任务与微任务
宏任务
宏任务(Macro Task):宏任务是由浏览器提供的任务,包括以下内容。宏任务被放入宏任务队列中,事件循环会依次取出宏任务执行。
-
setTimeout、setInterval 等定时器回调。
-
DOM 事件回调,如 click、load、change 等。
-
网络请求回调,如 XMLHttpRequest、fetch 等。
-
I/O 操作,如文件读写。
-
其他由浏览器调度的任务,如渲染事件。
微任务
微任务(Micro Task):微任务是由 JavaScript 引擎提供的任务,主要包括以下内容。微任务被放入微任务队列中,微任务队列在当前宏任务执行结束后、下一个宏任务执行之前执行。
-
Promise 的回调函数(.then()、.catch()、.finally())。
-
async/await 产生的任务。
-
MutationObserver 的回调函数。(
MutationObserver
是一种在现代浏览器中提供的接口,用于监视 DOM 树中发生的变化,并在检测到变化时异步地通知用户。它可以用来观察 DOM 节点的添加、删除、属性变化、文本内容变化等情况。)
事件循环(Event Loop)
事件循环:事件循环是 JavaScript 引擎用于管理异步任务执行顺序的机制。其工作流程如下:
- 执行同步任务:从调用栈顶开始执行同步代码,直到调用栈为空。
- 执行微任务:检查并执行微任务队列中的所有微任务,直到微任务队列为空。
- 更新渲染:如果需要,更新页面渲染。
- 执行宏任务:从宏任务队列中取出下一个宏任务执行,然后重复步骤 1。
事件循环代码示例
- 代码示例
console.log('Start'); // 同步代码 setTimeout(() => { // 宏任务 console.log('Timeout 1'); }, 0); Promise.resolve().then(() => { // 微任务 console.log('Promise 1'); }).then(() => { console.log('Promise 2'); }); setTimeout(() => { // 宏任务 console.log('Timeout 2'); }, 0); console.log('End'); // 同步代码
- 输出顺序
Start End Promise 1 Promise 2 Timeout 1 Timeout 2
- 执行步骤解析
-
同步任务 (1st):
console.log('Start')
被推入调用栈并执行,输出 ‘Start’,然后从调用栈弹出。setTimeout
设置两个定时器,将它们的回调函数添加到宏任务队列中。Promise.resolve()
将第一个.then()
回调添加到微任务队列中。console.log('End')
被推入调用栈并执行,输出 ‘End’,然后从调用栈弹出。
-
执行微任务 (2nd):
- 微任务队列中的第一个 Promise 回调被推入调用栈并执行,输出 ‘Promise 1’,然后从调用栈弹出。
- 第二个
.then()
回调添加到微任务队列中。 - 微任务队列中的第二个 Promise 回调被推入调用栈并执行,输出 ‘Promise 2’,然后从调用栈弹出。
-
执行宏任务 (3nd):
- 宏任务队列中的第一个
setTimeout
回调被推入调用栈并执行,输出 ‘Timeout 1’,然后从调用栈弹出。 - 宏任务队列中的第二个
setTimeout
回调被推入调用栈并执行,输出 ‘Timeout 2’,然后从调用栈弹出。
- 宏任务队列中的第一个
-