JS核心概念——事件循环

1,066 阅读7分钟

1.背景

image.png

简单来说,JS 的事件循环(Event Loop)机制,决定了什么时候该执行哪一行代码。虽然 JS 是单线程的,但浏览器是多线程的,所以 JS 借助浏览器提供的能力,实现了 JS 的异步编程(比如 setTimeout 方法)。如果没有 JS 事件循环,则当浏览器执行一段耗时的 JS 同步代码时,就没有办法及时响应用户的其他操作,从而给用户带来不好的体验。

2.事件循环的简单解释

2023-04-29 14-53-51 的屏幕截图.png

事件循环负责收集事件(如mousemove、setTimeout等),并对事件触发的回调任务进行排队以便在合适的时候执行。先执行宏任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染,如此循环往复。

3.事件循环的详细解释

2023-04-29 15-29-48 的屏幕截图.png

JS 的事件循环主要由这几部分组成:调用栈、web API、微任务队列、宏任务队列

事件循环机制

当 JS 主线程执行到同步方法时,会将该方法的执行上下文推入调用栈的栈顶。当执行到异步方法时(如 setTimeout),会将该方法交给其他线程(如定时器线程)来处理,待处理完后,将处理结果作为该异步方法的回调的参数,并和回调函数一起作为一个任务,放入任务队列等待 JS 主线程执行。若是微任务则放入微任务队列,若是宏任务则放入宏任务队列。当首轮循环中,调用栈中的任务都已执行完毕,JS 主线程会执行微任务队列中的任务,直到微任务队列为空,即使在执行微任务的过程中,有新的微任务产生,也会在本轮执行。然后再执行宏任务队列中的任务,执行宏任务的过程中,新产生的宏任务则会放到下一轮循环中执行。在每轮事件循环的末尾则可能会做一些渲染动作,执行宏任务、微任务的时候不会进行渲染。当所有代码都已执行完毕,JS 主线程则进入“休眠状态”等待新的任务产生,如此循环往复,从而形成事件循环。

调用栈

JS 调用栈(Call Stack)是一个用于记录函数调用的数据结构,它遵循先进后出的原则(LIFO)。当一个函数被调用时,它的执行上下文(包括函数参数、局部变量等)被添加到调用栈的顶部。当函数执行完毕时,它的执行上下文被从调用栈中弹出,控制权回到调用该函数的上一个执行上下文。

web API

JS 的 Web API 是浏览器提供的一组 API,用于与浏览器环境交互,包括 DOM、XMLHttpRequest、Fetch、setTimeout 等。这些 API 不是 JS 语言本身的一部分,而是浏览器提供的扩展,通过这些 API 可以让 JS 与浏览器环境进行交互。

微任务队列

微任务队列用于存放微任务,它遵循先进先出的原则(FIFO)。常见的微任务有:Promise.then 的回调、MutationObserver 的回调、queueMicrotask 的回调。当一个微任务被调度时,它被添加到微任务队列的末端,排队等待 JS 主线程的执行。

宏任务队列

宏任务队列用于存放宏任务,它遵循先进先出的原则(FIFO)。常见的宏任务有:setTimeout 的回调、 setInterval 的回调、requestAnimationFrame 的回调、IO事件。当一个宏任务被调度时,它被添加到宏任务队列的末端,排队等待 JS 主线程的执行。

4.例子

1.简单的例子

824b0c9a-9e74-4a9e-acd6-2a79cf1b4ab6.gif

  1. 执行 console.log('同步代码1'),为同步代码,直接将其执行上下文放入执行栈栈顶执行,控制台输出"同步代码1"后,将其执行上下文出栈销毁

  2. 执行setTImeout(() => { console.log('setTimeout') },0),为异步代码,交给定时器线程处理,定时结束后,将回调()=>{ console.log('setTimeout') }作为宏任务放入宏任务队列等待 JS 主线程执行

  3. 执行new Promise((resolve) => { console.log('同步代码2'); resolve(); }),为同步代码,直接将其执行上下文放入执行栈栈顶执行,先执行console.log('同步代码2'),控制台输出"同步代码2",再执行 resolve() ,将 then 方法的回调() => { console.log('promise.then') }放入微任务队列等待 JS 主线程执行,然后将该段代码的执行上下文出栈销毁

  4. 执行 console.log('同步代码3'),为同步代码,直接将其执行上下文放入执行栈栈顶执行,控制台输出"同步代码3"后,将其执行上下文出栈销毁

  5. 第一轮循环中的宏任务执行完毕,此时执行栈为空,去微任务队列按顺序取出微任务放入执行栈执行,此时微任务队列中只有() => { console.log('promise.then') },所以将其执行上下文放入执行栈栈顶执行 ,控制台输出 "promise.then"后,将其执行上下文出栈销毁

  6. 第一轮循环中的微任务执行完毕,此时执行栈为空,进入第二轮事件循环,去宏任务队列按顺序取出宏任务放入执行栈执行。此时宏任务队列中只有()=>{ console.log('setTimeout') },所以将其执行上下文放入执行栈栈顶执行 ,控制台输出 "setTimeout"后,将其执行上下文出栈销毁

  7. 至此所有代码执行完毕,控制台最终结果为:同步代码1 同步代码2 同步代码3 promise.then setTimeout,JS主线程进入“休眠状态”并等待新的任务出现...

2.复杂的例子

console.log(1);

setTimeout(() => {  //setTimeout-01
    console.log(2);
    queueMicrotask(() =>  { console.log(3); })
    new Promise((resolve) => {
        console.log(4);
        resolve();
    }).then(() => {  console.log(5); })
})

queueMicrotask(() => { console.log(6); })

new Promise((resolve) => {  //Promise-01
    console.log(7);
    resolve();
}).then(() => { console.log(8); })

setTimeout(() => {  //setTimeout-02
    console.log(9);
    queueMicrotask(() => { console.log(10); })
    new Promise((resolve) => {
        console.log(11);
        resolve();
    }).then(() => { console.log(12); })
})
  1. 执行 console.log(1);,控制台输出 1
  2. 执行 setTimeout-01,将其回调放入宏任务队列尾部
  3. 执行 queueMicrotask(() => { console.log(6); }),将其回调放入微任务队列尾部
  4. 执行Promise-01,控制台输出7,并将() => { console.log(8); }放入微任务队列尾部
  5. 执行setTimeout-02,将其回调放入宏任务队列尾部
  6. 首轮的宏任务执行完毕,开始执行微任务队中的微任务:控制台输出 6,控制台输出 8
  7. 首轮的微任务执行完毕,开始第二轮的事件循环,从宏任务队列头中取出setTimeout-01并执行
  8. 控制台输出2,并将() => { console.log(3); }放入微任务队列尾部
  9. 控制台输出4,并将() => { console.log(5); }放入微任务队列尾部
  10. 第二轮的宏任务执行完毕,开始执行微任务队中的微任务:控制台输出 3,控制台输出 5
  11. 第二轮的微任务执行完毕,开始第三轮的事件循环,从宏任务队列头中取出setTimeout-02并执行
  12. 控制台输出9,并将() => { console.log(10); }放入微任务队列尾部
  13. 控制台输出11,并将() => { console.log(12); }放入微任务队列尾部
  14. 第三轮的宏任务执行完毕,开始执行微任务队中的微任务,控制台输出 10,控制台输出 12
  15. 第三轮的微任务执行完毕, 至此,所有的代码执行完毕,JS主线程进入“休眠状态”并等待新的任务出现...

所以,控制台最终的输出结果顺序为:1 7 6 8 2 4 3 5 9 11 10 12

5.小技巧

  • 提高渲染性能:对于动画或需要高频率更新的场景,使用 requestAnimationFrame 能提高性能。
  • 避免耗时的同步任务:可以将耗时较长的同步任务,拆分成多个较小的异步任务(比如作为 setTimeout 的回调),以避免阻塞主线程。

6.参考资料

zhuanlan.zhihu.com/p/580956436

zh.javascript.info/microtask-q…

zh.javascript.info/event-loop

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…