事件循环

JavaScript 事件循环

单线程的 JavaScript

JavaScript 是单线程的,这意味着它一次只能执行一个任务。然而,由于 JavaScript 的异步特性,它可以同时处理多个任务,而不会阻塞主线程。

  • JavaScript 是单线程的,但是它通过事件循环机制实现了异步编程
  • 事件循环机制包括两个主要部分:调用栈和任务队列
  • 调用栈用于执行同步代码,任务队列用于处理异步代码
  • 当一个异步操作完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
  • 事件循环会不断检查任务队列,如果有任务就将其添加到调用栈中执行
  • 事件循环机制使得 JavaScript 可以在等待异步操作完成的同时继续执行其他代码,从而实现非阻塞的异步编程

同步和异步

同步:代码按照顺序执行,一个任务执行完才会执行下一个任务

异步:代码可以同时执行多个任务,一个任务执行完不会等待其他任务执行完,而是继续执行下一个任务

JavaScript 本身是单线程的,为了处理异步任务,宿主环境(浏览器 /v8)会将其交给其他线程处理, 执行

事件循环

事件循环是宿主环境处理 js 异步操作的方式,让其能够非阻塞式运行的机制

  • 浏览器进程

    • 主进程,无论打开多少个浏览器窗口,它仅有一个,负责浏览器界面显示、用户管理、进程管理等
  • 网络进程

    • 处理网站的数据请求和响应,网络进程内部会开启多个线程,以实现网络请求的异步处理
  • 渲染进程

    • 主要解析 html、css、js 等资源,并生成渲染树、执行布局、绘制,负责页面渲染

浏览器中的 Event Loop

  • 宏队列和微队列

    • 宏队列排队宏任务(DOM 操作回调,定时器回调,UI 绘制)
    • 微队列排队微任务(Promise 回调)

除了微队列外,队列的种类和数量可能不同,这取决与浏览器厂商

以 Chrome 为例:

  • 微队列:用于存放需要执行最快的任务,优先级最高,比如:promise.then(),MutationObserve
  • 交互队列:用于存放用户操作后产生的事件任务,优先级仅次于微队列
  • 延迟队列:用于存放定时器到达后的回调任务,优先级次于交互队列

人工合成的事件派发,即直接在代码里写的 dom.click()dispatchEvent()相对于浏览器而言并不是真正的用户交互,会被当作同步任务执行

执行栈和任务队列

JS 在解析一段代码的时候,会将同步代码顺序排在某个地方,即执行栈,然后依此执行里面的函数。

当遇到异步任务就交给其他线程处理,待当前执行栈所有的同步代码执行完成后,会从一个队列中取出已完成的异步任务的回调加入到执行栈中继续执行,遇到异步任务又交给其他线程......如此循环往复

宏任务和微任务

任务队列不止一个,根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro task)队列

事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列中是否有任务需要执行,如果没有,再去宏任务队列检查,如此往复

微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环

微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个

常见的宏任务

  • setTimeout()
  • setInterval()
  • setImmediate()

常见的微任务

  • Promise.then(),Promise.cathch()
  • new MutaionObserver()
  • processs.nextTick()
js 复制代码
console.info('同步代码1')
setTimeout(()={
    console.info('setTimeot')
})
new Promise((resolve)=>{
    console.info('同步代码2')
    resolve()
}).then(()=>{
    console.info('promise.then')
})
console.info('同步代码3')

宏任务和微任务的本质区别

对于 promise.then(微任务),当执行到promise.then的时候,浏览器引擎并不会将异步任务交给浏览器其他线程,而是将回调存放在自己的一个任务队列中,待当前执行栈执行完成后,立马去执行promise.then存放的队列 promise.then微任务本身没有多线程参与 setTimeout 有"定时等待"的任务,需要定时器现行执行。ajax 请求有"发送请求"这个任务,需要 HTTP 线程处理 宏任务特征: 有明确的异步任务需要执行回调,并且需要等待异步任务执行完成,比如setTimeoutsetIntervalajax微任务特征: 没有明确的异步执行任务,不需要等待异步任务执行完成,比如promise.thenprocess.nextTick

视图更新渲染

视图重绘之前会先执行 requestAnimationFrame回调

浏览器 JS 异步执行的原理

浏览器是多线程的,当 js 需要执行异步任务的时候,浏览器会启动另一个线程去执行这个任务

js 复制代码
const btn = document.querySelector("button");
function handleClick() {}

渲染进程启动后,会开启一个渲染主线程,它是浏览器中最繁忙的线程,负责处理各种任务

  • 解析 html、css,计算样式、布局,构建 DOM 树和 CSSOM 树

  • 处理涂层,绘制页面

  • 执行 js 代码,包括同步代码和异步代码

  • 调用栈(Call Stack)

    • 用于执行同步代码,当调用栈为空时,事件循环会从任务队列中取出一个任务执行
  • 任务队列(Task Queue)

    • 用于存放异步任务,当异步任务完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
  • 微任务队列(Microtask Queue)

Node.js 事件循环

基于 Libuv 实现的,Libuv 是一个跨平台的异步 I/O 库,它提供了事件循环、文件系统操作、网络操作等功能 Libuv: 一个用 C 语言实现的高性能解决单线程非阻塞异步 I/O 的开源库

Node.js Event Loop

  • 宏队列

    • timers(重要)

    • penging callback

      • 调用上一次事件循环没有在 pool 阶段立即执行,而延迟的 IO 回调函数
    • idle prepare

      • 仅供 nodejs 内部使用
    • poll(重要)

    • check(重要)

    • close callback

      • 执行所有注册 close 事件的回调函数
  • 微队列

    • nextTick
    • Promise

timers

定时器队列,负责处理setTimeoutsetInterval的回调函数 不管是nodejs还是浏览器,所有的定时器回调函数都不能精准保证到达时间后立即执行

  • 一是因为计算机硬件和底层操作系统
  • 二是pool阶段对timers阶段的深刻影响。因为在没有满足pool阶段的结束条件前,就无法进入下一次事件循环的timers阶段

Pool

pool成为轮询队列,该阶段会处理timerscheck队列外的绝大多数 IO 回调任务,比如文件读取、监听用户请求等 当事件循环到达该阶段,它的运行方式是:

  • 如果pool队列中有回调任务,则依此执行回调,直到队列清空

  • 如果pool队列中没有回调任务

    • 如果其他队列中后续可能会出现回调任务,则一直等待,等其他队列中后续的回调任务来临时,结束该阶段
    • 如果等待的时间超过预设的时间限制,则也会自动进入下一次事件循环
    • 若其他队列中后续不可能出现回调任务了,则立即结束该阶段,并在本轮事件循环完成后,退出node程序
js 复制代码
const fs = require("fs");
const start = Data.now();
setTimeout(() => {
    console.info("setTimeout exe", Data.now() - start);
}, 200);

fs.readFile("/index.js", "utf-8", (err, data) => {
    console.info("file read");
    const start = Data.now();
    while (Data.now() - start < 300) {}
});

check

check称为检查队列,负责处理setImmediate定义的回调函数 setImmediatenodejs特有的定时器,它会在当前事件循环的末尾执行回调函数,类似于setTimeout,但setImmediate的回调函数会在pool阶段结束后立即执行 在nodejs中,setImmediate的执行效率远远高于setTimeout,setImmediate的执行顺序无法预测

js 复制代码
setTimeout(() => {
  console.info("setTimeout");
}, 0);
setImmediate(() => {
  console.info("setImmediate");
}

nextTick

我们可以通过process.nextTick()将回调函数加入到nextTick队列中,和通过Promise.resolve().then()将回调函数加入到Promise队列,并且nextTick队列中优先级高于Promise队列。所有process.nextTick()是 nodejs 执行最快的异步操作

Promise 面试

  • Promise 特点
  • 事件循环
  • 解题思路
js 复制代码
console.info("1");
new Promise((resolve) => {
    resolve();
    console.info("2");
}).then(() => {
    console.info("3");
});
setTimeout(() => {
    console.info("4");
}, 0);
console.info("5");
  1. new Promise会立即执行,所以会先输出2
  2. resolve或者reject之后状态不再改变,但是后面代码会执行
js 复制代码
new Promise((res) => {
   res();
   console.info("test");
   reject();
});
  1. promisethen(catch)回调放入到微任务队列,setTimeout放入到宏任务队列
  2. 调用栈中代码执行完后,先去微任务队列中的任务执行,直到微任务队列为空
  3. 微任务队列为空,取宏任务队列中的一个任务开始执行,然后重复上一步,直到宏任务队列为空
相关推荐
EnCi Zheng11 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen15 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技16 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人27 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实28 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha39 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
不可能的是1 小时前
从 /simplify 指令深挖 Claude Code 多 Agent 协同机制
javascript