笔试题——JavaScript事件循环机制(event loop、macrotask、microtask)

本文详细探讨了JavaScript的事件循环机制,包括事件队列、Promise的工作原理,以及宏任务和微任务的区别。通过实例分析,帮助读者理解JavaScript异步执行的内部流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天做了一道笔试题觉得很有意义分享给大家,题目如下:

setTimeout(()=>{
    console.log('A');
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log('B')
        },0);
        return new Promise(function (resolve) {
            console.log('C');
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log('D')
});
console.log('E');复制代码

JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 。要完全理解异步,就需要了解 JS 的运行核心——事件循环(event loop)。

 一、什么是事件队列?

首先来看一个小小的demo

console.log('start');
setTimeout(()=>{
    console.log('A');
},1000);
console.log('end');
//start
//end
//A复制代码

js执行之后,程序输出 'start' 和 'end',在大约1s之后输出了 'A' 。那我们就有疑问了?为什么A不在end之前执行呢?

这是因为 setTimeout 是一个异步的函数。意思也就是说当我们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束之后,事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务过后便将任务压入执行栈(stack)当中,执行栈执行任务,输出 'A'。

事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。

那么我将这个例子做一个小小的改动看一看:

console.log('start');
setTimeout(()=>{
    console.log('A');
},0);
console.log('end');
//start
//end
//A复制代码

可以看出,我们将settimeout第二个参数设置为0后,'A' 也总是会在 'end' 之后输出。所以究竟发生了什么?这是因为 setTimeout 的回调函数只是会被添加至事件队列,而不是立即执行。由于当前的任务没有执行结束,所以 setTimeout 任务不会执行,直到输出了 'end' 之后,当前任务执行完毕,执行栈为空,这时事件队列才会把 setTimeout 回调函数压入执行栈执行。

 二、Promise的含义和基本用法?

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

写一个小demo看一下Promise的运行机制:

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');
// Promise
// Hi!
// resolved复制代码

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

 三、Macrotasks和Microtasks

Macrotasks和Microtasks 都属于上述的异步任务中的一种,他们分别有如下API:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver

setTimeout的macrotask, 和 Promise的microtask 有哪些不同,先来看下代码如下:

console.log(1);
setTimeout(function(){
  console.log(2);
}, 0);
Promise.resolve().then(function(){
  console.log(3);
}).then(function(){
  console.log(4);
});

//1
//3
//4
//2复制代码

如上代码可以看到,Promise的函数代码的异步任务会优先setTimeout的延时为0的任务先执行。

原因是任务队列分为 macrotasks 和 microtasks, 而promise中的then方法的函数会被推入到microtasks队列中,而setTimeout函数会被推入到macrotasks

任务队列中,在每一次事件循环中,macrotask只会提取一个执行,而microtask一直提取,直到microsoft队列为空为止。

也就是说如果某个microtask任务被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。

而事件循环每次只会入栈一个macrotask,主线程执行完成该任务后又会检查microtasks队列并完成里面的所有任务后再执行macrotask的任务。

 四、分析本题目

setTimeout(()=>{
    console.log('A');
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log('B')
        },0);
        return new Promise(function (resolve) {
            console.log('C');
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log('D')
});
console.log('E');复制代码

1、首先 setTimeout A 被加入到事件队列中  ==>  此时macrotasks中有[‘A’];

2、obj.func()执行时,setTimeout B 被加入到事件队列中  ==> 此时macrotasks中有[‘A’,‘B’];

3、接着return一个Promise对象,Promise 新建后立即执行 执行console.log('C'); 控制台首次打印‘C’;

4、然后,then方法指定的回调函数,被加入到microtasks队列,将在当前脚本所有同步任务执行完才会执行。 ==> 此时microtasks中有[‘D’];

5、然后继续执行当前脚本的同步任务,故控制台第二次输出‘E’;

6、此时所有同步任务执行完毕,如上所述先检查microtasks队列完成其中所有任务,故控制台第三次输出‘D’;

7、最后再执行macrotask的任务,并且按照入队列的时间顺序,控制台第四次输出‘A’,控制台第五次输出‘B’。

 五、执行js代码



分析与实际符合,NICE!

参考文章:www.cnblogs.com/tugenhua070…

还有阮老师的promise介绍:es6.ruanyifeng.com/?search=pro…

文章本人原创,转载请评论;

前端菜鸟对JavaScript的理解还有很多不足,如有错误欢迎大家指出来;

喜欢的点个赞把!


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

余额充值