JS事件循环机制

本文详细解析了HTML页面中点击事件的冒泡和捕获机制,以及JavaScript的EventLoop在事件处理中的作用,包括宏任务和微任务的执行顺序。通过实例演示,解释了如何理解事件循环的工作原理和代码执行流程。

前言

先上面试题:

<div class="grandma">奶奶
    <div class="mother">妈妈
        <div class="daughter">女儿
            <div class="baby">婴儿</div>
        </div>
    </div>
</div>
<script>
    var grandma = document.getElementsByClassName("grandma")[0];
    var mother = document.getElementsByClassName("mother")[0];
    var daughter = document.getElementsByClassName("daughter")[0];
    var baby = document.getElementsByClassName("baby")[0];

    baby.addEventListener("click", () => { console.log("I'm baby") }, false);
    daughter.addEventListener("click", () => { console.log("I'm daughter") }, true);
    mother.addEventListener("click", () => { console.log("I'm mother") }, true);
    grandma.onclick = () => { console.log("I'm grandma") };
</script>

根据如上代码,页面上点击baby之后,控制台会打印什么?

捕获和冒泡

上面这道题,每个dom上有不同的事件处理程序,就代表着有不同的事件流。考察的就是事件捕获和冒泡的执行流程问题,addEventListener(type, listener, useCapture)接受三个参数:

  1. type:监听的事件类型字符串
  2. listener:函数
  3. useCapture:布尔值,默认false代表冒泡,true代表捕获

因此,就可知道很简单的标注一下每种事件处理程序的事件流:

// 冒泡
baby.addEventListener("click", () => { console.log("I'm baby") }, false);
// 捕获
daughter.addEventListener("click", () => { console.log("I'm daughter") }, true);
// 捕获
mother.addEventListener("click", () => { console.log("I'm mother") }, true);
// 冒泡
grandma.onclick = () => { console.log("I'm grandma") };

在页面中点击一个div,事件流的过程如下图就能形象阐述:
在这里插入图片描述
从中也可以看出,事件捕获是优先于事件冒泡的,所以是先触发事件捕获,捕获流程是从最外层向最内层触发。因此可以确定,第一个触发mother,然后触发daughter。

事件冒泡则相反,由最内层向最外层触发,所以能确定,先触发baby,再触发grandma。

最后,结果就是1. mother,2. daughter,3. baby, 4. grandma

Event Loop

既然是讲事件这块知识的,怎能不提这个面试常考点呢。

理论派请问,说一下事件循环机制的原理。

实战派请问,如下这段代码的执行结果:

console.log("小红");
setTimeout(() => {
    console.log("小白");
}, 0)
console.log("小黄");

下面详述这段代码的执行流程,先确定几个东西:

  1. 控制台:代码最终执行结果输出在这儿
  2. 调用栈(Call Stack):LIFO(后进先出)结构,主要用来保存调用的返回地址
  3. Web APIs
  4. 回调函数队列(Callback Queue):FIFO(先进先出)结构,

现在按照代码执行顺序走一遍大白话:

第一步

JS代码是顺序执行,先走到console.log("小红");这儿,这时候会将这段代码放进调用栈,在这个地方去执行代码,这个代码就是打印一个小红,输出结果显示在控制台。这段代码执行完成,就会从调用栈里移除。

第二步

代码走到了setTimeout这儿,就将这一整块的代码放进调用栈执行代码,setTimeout是一个Web API,它的意思是0秒之后执行里面的回调函数。但是这一整块的代码此时在调用栈里是执行完毕,也就会从调用栈中移除。

第三步

代码来到console.log("小黄"),代码放进调用栈执行,打印小黄,在控制台输出结果,代码执行完毕,调用栈移除代码。

至此,所有的同步代码已经执行完毕。

第二步的后续完善

这时候,Event Loop登场,它不断循环回调函数队列,去寻找是否存在一些异步任务执行完后的回调函数,需要去执行。

在0秒以后,Web APIs里setTimeout就会把它里面的回调函数放入Callback Queue,Event Loop再去循环时发现了有待执行的回调函数,而且此时调用栈里是空的,Event Loop就会将找到的回调函数推入调用栈,来执行函数代码。

这个回调函数里console.log("小白");需要执行时,也是将这段代码放入调用栈,此时根据调用栈的LIFO结构,后入的代码先执行,打印小白,在控制台输出结果,代码执行完毕,调用栈移除这段代码。此时调用栈里还剩回调函数代码,但函数也执行完了,也从栈里移除。

总结

总结来说event loop的循环过程:

  1. 在宏任务队列(tasks队列)中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的微任务(microtasks)步骤。

  2. 将上边选择的task设置为正在运行的task。

  3. Run: 运行被选择的task。

  4. 将event loop的currently running task变为null。

  5. 从task队列里移除之前运行的task。

  6. Microtasks:执行microtasks任务检查点(也就是执行microtasks队列里的任务)。

  7. 更新渲染(Update the rendering)

  8. 返回到第一步。

注意

这题的迷惑点可能是那个0秒,如果换成3秒可能也会蒙对。其实只需要记住的是,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,顺便说一下,Promise.resolve()在本轮“事件循环”结束时执行,这就涉及到宏任务和微任务的知识点了。

任务

JS中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。

宏任务

一个event loop有一个或者多个task队列,当用户代理安排一个任务,必须将该任务增加到相应的event loop的一个tsak队列中,每一个task都来源于指定的任务源,比如可以为鼠标、键盘事件提供一个task队列,其他事件又是一个单独的队列。可以为鼠标、键盘事件分配更多的时间,保证交互的流畅。

譬如setTimeoutsetInterval,DOM事件,AJAX请求都是宏任务。

微任务

每一个event loop都有一个microtask队列,一个microtask会被排进microtask队列而不是task队列。

譬如Promiseasync/await都是微任务。

看下面这段代码,输出的结果是什么?

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

1和4是同步代码,先输出打印,2和3是异步代码会后打印。

微任务的执行时机比宏任务,所以3比2先打印。

再来个复杂点的题,如下代码会输出什么结果:

console.log('script start');
setTimeout(function() {
  console.log('timeout1');
}, 10);
new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})
console.log('script end');

来对上面的代码进行个流程详解。

首先,事件循环会从宏任务(MacroTask)队列开始,MacroTask中只有一个script包含的整块代码。

当遇到任务源(task source)时,分发任务到对应的任务队列中。第一个遇到到console.log('script start');,输出了script start到控制台。

继续往下走,遇到了setTimeout任务源,将它分发到MacroTask队列中,记为setTimeout_A。

遇到了Promisenew Promise中代码立即执行,在控制台总是输出promise1

然后执行resolve,遇到了setTimeout,将它分发到MacroTask队列中,记为setTimeout_B。

继续往下,将then分发到微任务(MicroTask)队列中,标记为then_A。

遇到了console.log('script end');,输出了script end到控制台。

当同步代码都执行完成,开始检查MicroTask队列,发现有个then_A,执行后输出then1,执行完毕移除,

MicroTask队列已清空。

开始检查MacroTask队列,发现了setTimeout_A和setTimeout_B,因为是先进先出结构,先执行setTimeout_A,输出了timeout1,然后执行setTimeout_B,输出了timeout2

MacroTask队列也清空,所有队列都是清空状态,执行结束。

<think>嗯,用户想了解JavaScript事件循环的工作原理,包括机制、原理图解。好的,首先我需要回忆一下事件循环的基本概念。JavaScript是单线程的,这意味着它一次只能执行一个任务。但是,为了处理异步操作,比如定时器、网络请求或者事件监听,就需要事件循环机制来管理这些任务的执行顺序。 根据引用[2],事件循环JavaScript实现异步的关键。用户可能想知道单线程如何同时处理多个任务。这里需要解释调用栈、任务队列事件循环之间的关系。同步代码会直接执行,而异步任务会被放到任务队列中,等待调用栈清空后再执行。 接下来,引用[4]提到事件循环模型是基于事件驱动的,浏览器生成的事件会被放入事件队列。这里可能需要区分宏任务微任务。比如,setTimeout属于宏任务,而Promise的then回调属于微任务。微任务的优先级高于宏任务,所以事件循环在处理宏任务之前会先清空微任务队列。 引用[5]中的图解应该可以帮助用户理解执行顺序。比如,先执行同步代码,遇到异步任务就注册回调,然后继续执行同步代码。等同步代码执行完后,检查微任务队列,执行所有微任务,再执行一个宏任务,如此循环。 用户可能需要具体的例子来说明这个过程。例如,同步代码、Promise、setTimeout执行顺序。这时候可以用一段代码示例,分步骤解释执行过程。同时,引用中的资料可以作为理论支持,比如引用[1]提到事件循环对编写高效异步代码的重要性,说明理解这一机制的实际应用。 另外,需要注意Node.js事件循环与浏览器的区别,如引用[3]提到两者的区别。但用户的问题可能主要集中在浏览器环境,所以可以简要提到Node.js的不同,但主要详细说明浏览器的机制。 最后,用户可能需要一个清晰的总结,将整个过程步骤化,便于记忆应用。同时,相关问题可以引导用户深入理解,比如事件循环的应用场景、宏任务微任务的区别,以及如何避免阻塞事件循环等。</think>### JavaScript事件循环机制详解 #### 1. 核心架构 JavaScript采用单线程+事件循环模型实现异步处理。执行顺序遵循**同步代码 > 微任务 > 宏任务**的优先级规则[^2][^5]。 #### 2. 核心组件图解 ``` ┌───────────────────────┐ │ Call Stack │<─────执行上下文 └──────────┬────────────┘ │ ▼ ┌───────────────────────┐ │ Web APIs │<─────浏览器提供的API └──────────┬────────────┘ │ ▼ ┌───────────────────────┐ │ Task Queue(宏任务) │ ├───────────────────────┤ │ Microtask Queue │ └──────────┬────────────┘ │ ▼ ┌───────────────────────┐ │ Event Loop │<─────循环检测机制 └───────────────────────┘ ``` #### 3. 执行流程(含代码示例) ```javascript console.log('1'); // 同步 setTimeout(() => console.log('2'), 0); // 宏任务 Promise.resolve().then(() => { console.log('3'); // 微任务 setTimeout(() => console.log('4'), 0); // 嵌套宏任务 }); console.log('5'); // 同步 ``` **执行顺序解析**: 1. 执行同步代码:输出`1``5` 2. 处理微任务队列:输出`3`,注册新宏任务`4` 3. 执行第一个宏任务:输出`2` 4. 执行新产生的宏任务:输出`4` 最终输出顺序:`1 → 5 → 3 → 2 → 4` #### 4. 关键特性对比表 | 任务类型 | 常见API | 执行优先级 | 队列清空方式 | |----------|-------------------------|------------|------------------| | 微任务 | Promise.then, MutationObserver | 高 | 完全清空队列 | | 宏任务 | setTimeout, DOM事件 | 低 | 每次执行一个任务 | #### 5. 性能优化要点 - 避免长任务阻塞主线程(超过50ms的任务会导致界面卡顿) - 使用`requestIdleCallback`处理低优先级任务 - Web Workers处理CPU密集型任务[^1][^4] #### 6. 浏览器与Node.js差异 浏览器环境下每轮事件循环包含: 1. 执行一个宏任务 2. 清空微任务队列 3. 执行渲染操作 Node.js使用libuv库实现,包含6个阶段: 1. timers 2. pending callbacks 3. idle/prepare 4. poll 5. check 6. close callbacks[^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值