基础概念
事件循环(Event Loop):事件循环是JavaScript运行时环境中的一个循环机制,它不断地检查调栈用和任务队列。当调用栈为空时,事件循环会首先检查微任务队列,并执行其中的所有任务。只有当微任务队列为空时,事件循环才会检查任务队列,并执行其中的任务。
- 同步代码首先执行。
- 宏任务和微任务分别被添加到各自的队列中。
- 执行栈为空后,先执行所有微任务,再执行一个宏任务。
- 事件循环不断重复这个过程,直到没有任务需要执行。
调用栈
为什么用栈?想想函数内调用其他函数,要等内部函数执行完成才继续执行剩余的外部函数。
在JavaScript中,调用栈(Call Stack)是一个LIFO(后进先出)结构,用于管理函数调用及其执行上下文。每当一个函数被调用时,一个新的执行上下文会被创建并推入调用栈中;当函数执行完毕后,其执行上下文会从调用栈中弹出。
以下是调用栈内容如何加入和移除的详细过程:
- 全局执行上下文:
- JavaScript代码开始执行时,会首先创建一个全局执行上下文(Global Execution Context)。这个上下文在整个程序的生命周期内始终存在,并且作为调用栈的底部。
- 函数调用:
- 当一个函数被调用时,会创建一个新的执行上下文(Function Execution Context),并将其推入调用栈中。
- 每个执行上下文包含以下三个主要部分:
- 变量对象(Variable Object, VO):存储变量和函数声明。在ES6之后,这个概念被更现代的词法环境(Lexical Environment)和变量环境(Variable Environment)所替代。
- 作用域链(Scope Chain):保证对上级作用域中的变量和函数的访问。
this
值:函数被调用时绑定的this
值。
- 执行上下文创建和推入调用栈:
- 创建一个新的执行上下文。
- 将这个新的执行上下文推入调用栈。
- 执行上下文中的变量和函数声明会被提升(hoisting)。
- 如果函数中有参数,参数也会被初始化。
this
值被确定。
- 函数执行:
- 执行函数体内的代码。
- 如果函数内部调用了其他函数,那么会重复步骤2和3,为被调用的函数创建新的执行上下文并推入调用栈。
- 函数完成:
- 当函数执行完毕后,其执行上下文会从调用栈中弹出。
- 如果函数返回了一个值,这个值会被传递给调用者。
- 调用栈为空:
- 当调用栈为空时,JavaScript引擎认为当前代码执行完毕,可能会开始执行事件循环中的任务(如异步回调)。
function outerFunction() {
console.log('Outer function start');
function innerFunction() {
console.log('Inner function start');
// Some code...
console.log('Inner function end');
}
innerFunction();
console.log('Outer function end');
}
outerFunction();
调用栈的变化:
- 全局执行上下文被推入调用栈。
outerFunction
被调用,创建outerFunction
的执行上下文并推入调用栈。outerFunction
打印Outer function start
。innerFunction
被调用,创建innerFunction
的执行上下文并推入调用栈。innerFunction
打印Inner function start
和Inner function end
。innerFunction
执行完毕,其执行上下文从调用栈中弹出。outerFunction
继续执行,打印Outer function end
。outerFunction
执行完毕,其执行上下文从调用栈中弹出。- 全局执行上下文始终是调用栈的底部,此时调用栈为空。
宏任务和微任务
- 微任务队列:它专门用于处理如
Promise
的resolve
或reject
回调、async/await、和
MutationObserver等微任务。微任务的优先级高于宏任务。 - 宏任务队列:用来存储准备好执行的回调函数,比如
setTimeout
和setInterval
的回调
Promise.resolve().then(() => {
console.log('outerPromise');
const innerTimer = setTimeout(() => {
console.log('innerTimer')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('outerTimer')
Promise.resolve().then(() => {
console.log('innerPromise')
})
}, 0)
console.log('run');
JavaScript代码示例
javascript复制代码
console.log('Script start');
setTimeout(() => {
console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('Promise callback');
});
console.log('Script end');
事件循环执行过程描述
- 全局执行上下文创建:
- 当JavaScript引擎开始执行这段脚本时,它会首先创建一个全局执行上下文。这个上下文包含了全局对象(在浏览器中通常是
window
对象)以及脚本中声明的所有变量和函数。
- 当JavaScript引擎开始执行这段脚本时,它会首先创建一个全局执行上下文。这个上下文包含了全局对象(在浏览器中通常是
- 同步代码执行:
- 引擎开始执行全局执行上下文中的同步代码。
- 首先,打印出
'Script start'
。
- 宏任务队列(Macro Task Queue):
- 当遇到
setTimeout
时,JavaScript引擎不会立即执行其回调函数,而是将回调函数包装成一个宏任务,并将其添加到宏任务队列中。setTimeout
的延迟时间设置为0,但这并不意味着回调函数会立即执行;它只会在下一个事件循环迭代中被执行。
- 当遇到
- 微任务队列(Micro Task Queue):
- 当遇到
Promise.resolve().then(...)
时,then
方法中的回调函数会被包装成一个微任务,并添加到微任务队列中。微任务队列中的任务会在当前执行栈为空后、下一个宏任务执行之前被立即执行。
- 当遇到
- 继续同步代码执行:
- 接下来,打印出
'Script end'
。 - 此时,全局执行上下文中的同步代码已经执行完毕,执行栈为空。
- 接下来,打印出
- 微任务执行:
- 在执行下一个宏任务之前,JavaScript引擎会检查微任务队列。由于我们有一个微任务(即
Promise
的回调函数),引擎会执行这个微任务。 - 打印出
'Promise callback'
。
- 在执行下一个宏任务之前,JavaScript引擎会检查微任务队列。由于我们有一个微任务(即
- 宏任务执行:
- 微任务队列为空后,JavaScript引擎会从宏任务队列中取出下一个任务来执行。在这个例子中,宏任务队列中有一个由
setTimeout
添加的回调函数。 - 执行这个回调函数,并打印出
'Timeout callback'
。
- 微任务队列为空后,JavaScript引擎会从宏任务队列中取出下一个任务来执行。在这个例子中,宏任务队列中有一个由
- 事件循环继续:
- 如果此时没有其他宏任务或微任务需要执行,事件循环可能会等待新的异步事件(如用户输入、网络请求等)来触发新的任务添加到任务队列中。
- 一旦有新的事件触发,相应的回调函数会被添加到宏任务队列或微任务队列中,并等待事件循环的下一个迭代来执行。