JS中的Event Loop 事件循环机制

本文深入探讨了JavaScript中的Event Loop事件循环机制,从浏览器进程的多线程模型开始,包括渲染进程、GUI渲染线程、JS引擎线程、事件触发线程和定时触发线程。讲解了宏任务与微任务的概念及其执行顺序,阐述了JS单线程设计的原因。

前言

Event Loop 也是 JavaScript 的重点知识之一,为了讲清楚,我会从浏览器进程,其中的渲染进程包含的线程等方面,来仔细说明。各个知识点都是相关联的,简单地说出宏任务和微任务并不能帮我们很好地理解整个事件循环机制。

浏览器是多进程的

浏览器进程可以简单分为以下几点:

  • Browser 进程——浏览器的主进程,负责协调和主控,该进程只有一个。它可以控制页面的显示,和与用户的交互。负责各个页面的管理,创建和销毁进程。可以将渲染进程得到的内存中的 Bitmap(位图)绘制到用户界面上。同时它还负责网络资源的下载管理。
  • 第三方插件进程。该进程与第三方插件是一对一的关系,在使用的时候才会创建。
  • GPU进程。负责3D绘制。
  • 渲染进程——即我们所说的浏览器内核。每个 Tab 页面都有一个渲染进程,互不影响。它主要用于页面渲染、脚本执行、事件处理。

渲染进程

渲染进程主要包含以下几个线程:

  1. GUI渲染线程
    负责浏览器渲染界面,解析 HTML、CSS,生成 RenderTree。负责回流和重绘。
    与JS引擎线程互斥,在JS引擎线程执行时,GUI 渲染线程会挂起(冻结),保存在一个队列中,等到JS引擎线程空闲的时候,立即被执行。

  2. JS引擎线程
    JS内核。负责处理JS脚本。等待执行任务队列中的任务。一个Tab页面只有一个JS线程。与GUI渲染线程互斥,会阻塞GUI引擎渲染。当浏览器渲染时遇到script标签,会停止GUI渲染,等到JS引擎执行完毕停止工作,再继续进行GUI渲染。

  3. 事件触发线程
    属于浏览器而不是属于JS引擎。用来控制事件循环,管理一个事件队列。
    我们知道JS是单线程的,为了避免阻塞,当JS执行到事件绑定和一些异步操作的时候(比如SetTimeout,或可能来自浏览器其他线程的鼠标点击,AJAX等),会通过事件触发线程将事件添加到对应的线程中,如定时器事件=>定时器线程。等到异步事件有了结果,再把他们的回调添加到事件队列中,等待JS引擎线程空闲时处理。

  4. 定时触发线程
    SetTimeout,SetInterval 所在线程。浏览器定时计数器通过单独线程来计时并触发定时,计时完毕后添加到事件触发线程队列中,等到JS引擎空闲执行。

  5. 异步HTTP请求线程
    在 XMLHTTPRequest 连接后通过浏览器新开一个线程,检测到状态发生变化时,若有回调函数,异步线程就产生状态变更事件,将回调放入事件队列由JS引擎执行。

所有线程的工作都单一且独立。如定时器线程只关心定时不关注结果,时间到了就把回调给事件触发线程,事件触发线程只关心异步回调入事件队列,而JS引擎线程只会执行执行栈中的事件,执行完毕之后就会读取事件队列中的事件添加到执行栈中执行。

事件循环

上面我们说了分工不同的各个线程,现在我们就来捋捋这个事件循环。

同步任务都在主线程(JS引擎线程)上执行,形成一个执行栈。
主线程之外,事件触发线程管理着一个任务队列,异步任务有运行结果时,就将事件回调放入任务队列。
执行栈中的同步任务执行完毕,JS引擎线程就会读取任务队列,添加到执行栈中,开始执行。

宏任务

每次执行栈执行的代码,可看作一次宏任务,每一个宏任务会从头到尾执行完毕,不会执行其他。宏任务包括:

  • 主代码块、setTimeout、setInterval、setImmediate、requestAnimationFrame (浏览器)

微任务

可理解为在当前宏任务执行后立即执行的任务。一个宏任务会在渲染前将执行期间产生的所有微任务执行完。微任务包括:

  • process.nextTick()、promise.then()、catch、finally、object.observe、Mutation Observer

执行顺序可以总结为:
宏任务 => 微任务 => GUI渲染 => 宏任务 …(循环)

  1. 执行一个宏任务(栈中没有就从任务队列中获取)。
  2. 执行时遇到微任务添加到微任务队列中。
  3. 宏任务执行完毕后,立即执行微任务队列中的所有微任务。
  4. 开始检查渲染,GUI线程接管渲染。
  5. 渲染完毕后,JS引擎线程继续接管,开始下一个宏任务。

其他

JS单线程的原因

① 创建 JS 的时候,多线程并不流行,也缺乏足够的硬件支持。
② 多线程更复杂,需加锁,操作DOM可能会导致预期之外的结果。

<think>好的,我现在需要详细解释JavaScript事件循环机制的工作原理。首先,用户提供的引用资料已经涵盖了基本概念和部分运行机制,我需要整合这些信息,并确保结构清晰,符合用户的要求。 首先,根据引用[2],JavaScript是单线程的,这意味着它一次只能处理一个任务。这种设计避免了多线程操作DOM可能导致的竞争问题,但同时也带来了阻塞的风险。为了解决这个问题,JavaScript引入了事件循环机制,如引用[1]和[4]提到的,通过非阻塞I/O和异步处理来管理任务。 接下来,事件循环的基本结构需要明确。引用[4]指出,浏览器是多线程的,DOM事件、AJAX、setTimeout等由单独的线程处理。主线程执行同步代码,遇到异步任务时,将其交给对应的Web API处理。完成后,回调函数被放入任务队列,等待事件循环将它们推入调用栈执行。 任务队列的分类也很重要。引用[4]提到microtasks和macrotasks的区别。Microtasks包括Promise和MutationObserver,而macrotasks包括setTimeout、setInterval和I/O操作。事件循环在每一轮循环中会先处理所有microtasks,再处理一个macrotask,确保高优先级的任务优先执行。 然后,结合引用[3]和[5],需要说明事件循环的具体步骤:执行同步代码、处理microtasks队列、渲染UI(如果需要)、处理macrotasks队列中的一个任务,然后循环往复。例如,当多个setTimeout延时相同时,它们会被合并处理,但执行时仍按顺序进入队列。 最后,要确保回答中正确引用来源,并在末尾生成相关问题。需要检查LaTeX格式是否符合要求,特别是行内公式用$...$,独立公式用$$...$$。同时,保持中文回答,结构清晰,分点解释,帮助用户逐步理解机制。</think>### JavaScript事件循环机制详解 JavaScript的**事件循环Event Loop)**是支撑其异步编程的核心机制,解决了单线程环境下的非阻塞问题。以下是其工作原理的分步解析: --- #### 一、核心概念 1. **单线程模型** JavaScript设计之初是单线程的,即只有一个主线程(Main Thread)执行代码[^2]。这意味着所有任务必须排队执行,无法同时处理多个操作。例如: ```javascript console.log("1"); setTimeout(() => console.log("2"), 0); console.log("3"); // 输出顺序:1 → 3 → 2 ``` 即使`setTimeout`延时为0,其回调仍会等待同步代码执行完毕。 2. **非阻塞的实现** 通过**事件循环**,JavaScript将耗时操作(如网络请求、定时器)交给浏览器或Node.js的其他线程处理,主线程继续执行后续代码。完成后,异步任务的回调被推入任务队列,等待执行[^1][^4]。 --- #### 二、事件循环的组成 1. **调用栈(Call Stack)** 用于执行同步代码,遵循后进先出(LIFO)原则。当遇到异步任务时,将其移交给**Web API**处理。 2. **任务队列(Task Queue)** - **宏任务队列(Macrotask Queue)**:包含`setTimeout`、`setInterval`、I/O操作等。 - **微任务队列(Microtask Queue)**:包含`Promise.then`、`MutationObserver`等。 3. **事件循环流程** 每次循环按以下顺序执行: ```plaintext 1. 执行调用栈中的同步代码。 2. 清空微任务队列中的所有任务。 3. 执行一个宏任务。 4. 重复循环。 ``` 例如: ```javascript setTimeout(() => console.log("宏任务"), 0); Promise.resolve().then(() => console.log("微任务")); console.log("同步代码"); // 输出顺序:同步代码 → 微任务 → 宏任务 ``` --- #### 三、关键机制 1. **微任务优先级高于宏任务** 每次事件循环中,微任务队列会在渲染前被完全清空。这意味着微任务可以阻塞渲染,而宏任务不会。 2. **浏览器渲染时机** 浏览器在每轮事件循环结束后,根据是否需要更新界面决定是否渲染(约60帧/秒)。 3. **Node.js与浏览器的差异** Node.js事件循环分为多个阶段(如`timers`、`poll`、`check`),而浏览器环境更简化[^3]。 --- #### 四、示例分析 ```javascript console.log("Start"); setTimeout(() => console.log("Timeout"), 0); Promise.resolve().then(() => console.log("Promise")); console.log("End"); // 输出顺序: // Start → End → Promise → Timeout ``` **执行步骤**: 1. 同步代码`console.log`依次执行。 2. `setTimeout`回调进入宏任务队列。 3. `Promise.then`回调进入微任务队列。 4. 清空微任务队列,输出“Promise”。 5. 执行宏任务队列中的`setTimeout`回调。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值