JavaScript 运行机制 Event Loop(事件循环)详解

1. JavaScript 为什么是单线程的?

JavaScript 单线程主要和它的用途有关,它的主要作用是与用户交互以及处理 DOM,试着想一下如果 JavaScript 是多线程的,一个线程要给一个 DOM 添加元素,另一个线程要删了这个 DOM,(浏览器一脸懵逼)。所以为为了避免出现这种情况,JavaScript 只能是单线程,也是这门语言的核心。

虽然说后来的 HTML5 提出了 Web Worker 标准,允许 JavaScript 创建多线程,但是这里的子线程完全受主线程控制,并且不能操作 DOM 。这个标准并没有改变 JavaScript 的本质。

2. 同步任务和异步任务

如果都是单线程,那么就会出现一个问题,所有任务都要排队处理,而有的任务执行时间特别的慢(比如 IO 设备),并且 CPU 还有很多空闲的地方,这个时候,还是必须等待上一个任务执行完毕才执行下一个任务。

于是任务就分为两种,同步任务(synchronous)和异步任务(asynchronous)。

  • 同步任务:在主线程上排队的任务,只有上一个任务执行完毕,才会执行下一个任务。
  • 异步任务:不进入主线程,进入任务队列(task queue),只有 “任务队列” 通知主线程,某个异步任务可以执行了(并且主线程当前栈已经清空了),该任务才会进入主线程执行。

3. 事件循环机制

在这里插入图片描述
上图就是事件循环机制的示例图(JS 运行机制)。

  1. 首先 JavaScript 主线程运行的时候,产生了堆和栈,这个时候是同步环境。
  2. 往下执行的时候如果遇到异步任务,就会指给对应的异步进程处理(异步 API)
  3. 当异步任务(Ajax 值返回,Timers 时间到达)处理完毕后,会推入任务队列。当异步任务处理完毕后,会推入任务队列。
  4. 当主线程栈清空的时候(执行完了),会去查看任务队列是否有执行完毕的任务,如果有,就拿出一个任务推入主线程执行(先进先出)
  5. 重复执行 2、3、4 的这个过程就被称为事件循环。

其中异步任务有:

  • clickloaddone 等由浏览器内核的 DOM binding 模块处理,事件触发时,回调函数添加到任务队列中。
  • Ajax 网络请求,由浏览器内核模块 NetWork 模块处理,网络请求返回后,回调函数添加到任务队列。
  • setTimeoutsetInterval 等,由浏览器内核 Timer 模块处理,时间一到,回调函数添加到任务队列。 .

4. 任务队列

如上示意图,任务队列存在多个,同一任务队列内,按队列顺序被主线程取走;不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O);

4.1 任务队列的类型

任务队列存在两种类型,一种为 microtask queue,另一种为 macrotask queue
图中所列出的任务队列均为 macrotask queue ,而ES6 的 promise[ECMAScript标准]产生的任务队列为microtask queue`。

4.2 两者的区别

microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;
macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。

4.3 更完整的事件循环流程

将microtask加入到JS运行机制流程中,则:

  • step1、2、3同上,
  • step4:主线程查询任务队列,执行 microtask queue,将其按序执行,全部执行完毕;
  • step5:主线程查询任务队列,执行 macrotask queue,取队首任务执行,执行完毕;
  • step6:重复step4、step5。

microtask queue中的所有callback处在同一个事件循环中,而macrotask queue中的callback有自己的事件循环。

简而言之:同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环->(microtask queue的All)-> 事件循环2(macrotask queue中的一个)…

利用 microtask queue 可以形成一个同步执行的环境,但如果 Microtask queue 太长,将导致 Macrotask 任务长时间执行不了,最终导致用户I/O无响应等,所以使用需慎重。

5. 示例、验证

 console.log('1, time = ' + new Date().toString())
 setTimeout(macroCallback, 0);
  new Promise(function(resolve, reject) {
      console.log('2, time = ' + new Date().toString())
      resolve();
      console.log('3, time = ' + new Date().toString())
  }).then(microCallback);

  function macroCallback() {
      console.log('4, time = ' + new Date().toString())
  } 

  function microCallback() {
      console.log('5, time = ' + new Date().toString())
  }     

结合第二节与第三节的分析,此处的执行流程应为:
同步环境:1 -> 2 -> 3
事件循环1(microCallback):5
事件循环2(macroCallback):4
运行结果如下:
在这里插入图片描述

运行结果与预期一致,验证了在不同类型的任务队列中,microtask queue中的callball将优先执行。

总结:由此我们了解事件循环的机制,同时了解了任务队列、JS主线程、异步操作之间的相互协作;同时认识了两种任务队列:macrotask queue、microtask queue,它们由不同的标准制定,microtask queue对应ECMAScript的promise属性(ES6)和 DOM3的MutationObserver,文中说明了两者在事件循环中的运行情况及区别;在今后的异步操作中,通过灵活运用不同的任务队列,提升用户交互性能,给出更加的响应和视觉体验;同时,通过JS的事件循环机制,可以更清楚JS代码的执行流,从而更好的控制代码,更有效、更好的为业务服务。

参考:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://www.cnblogs.com/hity-tt/p/6733062.html

<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、付费专栏及课程。

余额充值