面试官:说说Event Loop(事件循环)

前言

大家都知道JavaScript是一种单线程的脚本语言。在任意时刻,JavaScript都只能执行一个任务。为了更有效地处理事件、用户交互、脚本执行、UI渲染和网络请求,而不阻塞主线程,Event Loop(事件循环机制)应运而生。

为什么是单线程?

单线程同时间只能做一件事。那为什么不能有多个线程呢?这样能提高效率啊。

JavaScript最初被设计为浏览器的脚本语言,其核心功能之一是操作DOM。

假设 JavaScript 同时有两个线程,并且同时对同一个dom进行操作,这时浏览器要应该以哪个线程为准?为了避免这种问题,所以js设计为单线程语言以避免这类问题。

同步与异步的执行

同步任务:同步任务直接在主线程上执行,一个接一个地进行。

异步任务 :会被加入到任务队列中,当主线程上的同步任务执行完毕,系统会按顺序执行队列中的异步任务,这种机制有效避免了在等待异步操作(如Ajax请求)期间的主线程阻塞。

执行栈与任务队列

执行栈

当我们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。

这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数,以及这个作用域中定义的变量和 this 的指向,而当一系列方法被依次调用的时候。由于 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈

任务队列

主线程会不停的从执行栈中读取事件,同步代码则立即执行 。当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件放置另一个队列中,即任务队列(Task Queue)。

当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么事件队列便依次将任务压入执行栈中运行。

宏任务与微任务:

异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

宏任务(macro task)

宏任务(macro task) 指的是浏览器在执行代码的过程中会调度的任务,比如事件循环中的每一次迭代、setTimeout 和 setInterval 等。宏任务会在浏览器完成当前同步任务之后执行。

常见宏任务:script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)

微任务(micro task)

微任务(micro task) 指的是在当前宏任务执行完成之后立即执行的任务,比如 Promise 的回调函数、process.nextTick 等。

常见微任务:Promise、 MutaionObserver、process.nextTick(Node.js环境)

Event Loop(事件循环):

    1. 进入到script标签,就进入到了第一次事件循环.
    1. 遇到同步代码,立即执行
    1. 遇到宏任务,放入到宏任务队列里.
    1. 遇到微任务,放入到微任务队列里.
    1. 执行完所有同步代码
    1. 查看是否有微任务,如果有则将代码放入主线程,新写入主线程的代码执行完毕后,查看是否还有微任务,依次执行,直至清空。
    1. 更新render(每一次事件循环,浏览器都可能会去更新渲染)
    1. 查看是否有宏任务,如果有则执行,将其代码放入主线程,重复步骤2。
    • 以此反复直到清空所有宏任务,这种不断重复的执行机制,就叫做事件循环

示例代码

js 复制代码
console.log("Start");

setTimeout(() => {
  console.log("Timeout1");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise 1");
}).then(() => {
  for(let i = 0 ; i < 9999; i++){
      console.log(`我是for循环的第${i}个`)
  }
});

setTimeout(() => {
  console.log("Timeout2");
},0);


console.log("End");

分析过程

  1. console.log("Start");:同步任务会立即执行,在控制台输出 "Start"。

  2. setTimeout(() => { console.log("Timeout1"); }, 0);:会被放入宏任务队列中。

  3. Promise.resolve().then(() => { console.log("Promise 1"); }):会被放入微任务队列,因为Promise.resolve()为同步执行,会返回一个已解析(resolved)的 Promise 对象,Promise对象调用then中的回调函数会被放入微任务队列中。

  4. .then(() => { for(let i = 0 ; i < 9999; i++){ console.log(我是for循环的第${i}个) } });: 会被放置微任务队列,因为then中的回调函数会被放入微任务队列中。(秉承着队列的"先进先出"的属性,大量的 "我是for循环的第i个" 会在Promise1之后输出)

  5. setTimeout(() => { console.log("Timeout2"); }, 0);:会被放入宏任务,此前宏任务中已有Timeout1,宏任务也是队列,一样是先进先出,所以Timeout2会在之前进入的Timeout1之后输出。

  6. console.log("End");:同步任务立即执行,在控制台输出 "End"。

输出结果:

js 复制代码
Start
End
Promise1
我是for循环的第0个
我是for循环的第1个
.............
我是for循环的第9998个
Timeout1
Timeout2

注意:微任务始终优先于宏任务执行,而在微任务队列中的任务会一直执行完毕,直到队列为空,然后才会执行下一个宏任务。这种执行机制是事件循环的关键特性。

结论

通过理解JavaScript的单线程本质、执行栈与任务队列的工作方式以及任务与微任务的区别,我们可以更好地编写高效且可靠的JavaScript代码。事件循环机制是这一切的核心,它确保了代码的有序执行和非阻塞交互。

相关推荐
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜3 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点3 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow3 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o3 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā4 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年6 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder6 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript