普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月1日首页

面试官 : “ 说一下 localhost 和127.0.0.1 的区别 ? ”

作者 千寻girling
2026年1月1日 17:01

localhost 是主机名(域名) ,属于应用层概念;

127.0.0.1 是IPv4 回环地址,属于网络层概念。

两者都用于访问本机服务,但 localhost 必须通过解析才能映射到具体 IP(默认是 127.0.0.1 或 IPv6 的 ::1),而 127.0.0.1 是直接的网络层标识,无需解析。


一、本质定义与协议层次

概念 localhost 127.0.0.1
本质 互联网标准规定的特殊主机名(RFC 6761 定义) IPv4 协议规定的回环地址(RFC 5735 定义)
协议层次 应用层(DNS 协议解析范畴) 网络层(IP 协议寻址范畴)
归属 属于域名系统(DNS) 属于 IP 地址体系
默认映射 IPv4: 127.0.0.1;IPv6: ::1 仅 IPv4 回环网段(127.0.0.0/8)的第一个地址

关键补充

  1. 127.0.0.0/8 网段:不只是 127.0.0.1,整个 127.x.x.x 网段(共 16777216 个地址)都属于回环地址,访问任何一个都会指向本机。
  2. localhost 的特殊性:它是一个保留主机名,不能被注册为公共域名,且操作系统会优先通过 hosts 文件解析,而非公共 DNS 服务器。

二、解析流程的根本差异

这是两者最核心的区别 ——是否需要解析,以及解析的顺序

1. localhost 的解析流程(应用层 → 网络层)

当你在浏览器输入 http://localhost:3000 时,操作系统会执行以下步骤:

  1. 检查本地 hosts 文件

    • Windows 路径:C:\Windows\System32\drivers\etc\hosts
    • Linux/macOS 路径:/etc/hosts
    • 如果 hosts 文件中有如下映射:127.0.0.1 localhost 或 ::1 localhost,则直接使用对应的 IP。
  2. 若 hosts 文件无映射,查询本地 DNS 缓存

    • 操作系统会检查之前是否解析过 localhost,若有缓存则直接使用。
  3. 若缓存无结果,查询本地 DNS 服务器

    • 但由于 localhost 是保留主机名,公共 DNS 服务器通常也会返回 127.0.0.1 或 ::1
  4. 解析完成后,转换为 IP 地址进行网络请求

    • 此时才进入网络层,使用解析后的 IP 连接本机服务。

2. 127.0.0.1 的访问流程(直接进入网络层)

当你输入 http://127.0.0.1:3000 时,跳过所有解析步骤

  1. 操作系统直接识别这是一个 IPv4 回环地址。
  2. 直接将网络请求发送到本机的网络接口(回环接口,lo 接口)。
  3. 目标服务监听 127.0.0.1 或 0.0.0.0 时,即可响应请求。

三、功能与使用上的具体差异

1. 协议支持差异

  • localhost:支持 IPv4 和 IPv6 双协议

    • 若你的系统开启了 IPv6,localhost 可能优先解析为 ::1(IPv6 回环地址)。
    • 例如:在 Node.js 中,server.listen(3000, 'localhost') 会同时监听 IPv4 的 127.0.0.1:3000 和 IPv6 的 ::1:3000
  • 127.0.0.1仅支持 IPv4

    • 无论系统是否开启 IPv6,使用 127.0.0.1 都只会走 IPv4 协议。
    • 例如:server.listen(3000, '127.0.0.1') 仅监听 IPv4 地址。

2. 性能差异

  • 127.0.0.1 略快:因为跳过了 DNS 解析流程(即使是本地 hosts 文件解析,也需要一次文件读取和匹配)。
  • 差异极小:在开发环境中,这种性能差异几乎可以忽略不计,除非是高频次的请求(如每秒上万次)。

3. 服务监听的差异

服务端程序的监听地址,会影响是否能被 localhost 或 127.0.0.1 访问:

监听地址 能否被 localhost 访问 能否被 127.0.0.1 访问 能否被局域网其他设备访问
localhost ✅(IPv4 解析时)
127.0.0.1 ✅(解析为 127.0.0.1 时)
0.0.0.0 ✅(通过本机局域网 IP)
::1(IPv6) ✅(解析为 ::1 时)

4. 自定义映射的差异

  • localhost 可以被自定义映射

    • 你可以修改 hosts 文件,将 localhost 映射到任意 IP,例如:

      192.168.1.100   localhost
      
    • 此时访问 localhost 会指向局域网的 192.168.1.100,而不是本机。

  • 127.0.0.1 无法被自定义

    • 它是 IPv4 协议规定的回环地址,无论如何修改配置,访问 127.0.0.1 都只会指向本机。

5. 兼容性差异

  • 老旧系统 / 服务:某些非常古老的程序(如早期的 DOS 程序、嵌入式设备程序)可能不识别 localhost 主机名,但一定能识别 127.0.0.1
  • IPv6 专属服务:某些服务仅监听 IPv6 的 ::1,此时只能通过 localhost 访问(解析为 ::1),而 127.0.0.1 无法访问。

四、实际开发中的选择建议

  1. 优先使用 localhost

    • 理由:兼容性更好,支持双协议,符合开发习惯,且无需关心 IPv4/IPv6 配置。
    • 场景:本地开发、测试环境、前端代理配置(如 Vite、Webpack 的 devServer.host: 'localhost')。
  2. 使用 127.0.0.1 的场景

    • 强制使用 IPv4:当服务仅监听 IPv4 地址,或系统 IPv6 配置有问题时。
    • 避免自定义映射:当你怀疑 hosts 文件被修改,localhost 被映射到非本机地址时。
    • 某些工具的特殊要求:部分 CLI 工具或服务(如某些数据库客户端)默认只识别 127.0.0.1
  3. 特殊场景:0.0.0.0

    • 这不是回环地址,而是通配地址,表示监听本机所有网络接口(包括回环接口、局域网接口、公网接口)。
    • 场景:需要让局域网其他设备访问本机服务时(如手机测试前端页面)。

五、验证两者差异的小实验

实验 1:修改 hosts 文件,观察 localhost 映射

  1. 打开 /etc/hosts(Linux/macOS)或 C:\Windows\System32\drivers\etc\hosts(Windows)。
  2. 添加一行:192.168.1.1 localhost
  3. 执行 ping localhost,会发现 ping 的是 192.168.1.1,而非 127.0.0.1
  4. 执行 ping 127.0.0.1,仍然 ping 本机。
  5. 恢复 hosts 文件默认配置:127.0.0.1 localhost 和 ::1 localhost

实验 2:查看服务监听的地址

  1. 在 Node.js 中运行以下代码:

    const http = require('http');
    const server = http.createServer((req, res) => {
      res.end('Hello World!');
    });
    // 监听 localhost
    server.listen(3000, 'localhost', () => {
      console.log('Server running on localhost:3000');
    });
    
  2. 执行 netstat -tulpn | grep 3000(Linux/macOS)或 netstat -ano | findstr 3000(Windows)。

  3. 会发现服务同时监听 127.0.0.1:3000 和 ::1:3000(IPv4 + IPv6)。

  4. 若将监听地址改为 127.0.0.1,则仅监听 127.0.0.1:3000


六、总结:核心区别一览表

对比维度 localhost 127.0.0.1
本质 主机名(域名) IPv4 回环地址
协议层次 应用层(DNS) 网络层(IP)
解析需求 必须解析(hosts → DNS) 无需解析
协议支持 IPv4 + IPv6 仅 IPv4
自定义映射 可通过 hosts 文件修改 不可修改,固定指向本机
服务监听 可同时监听 IPv4/IPv6 仅监听 IPv4
兼容性 现代系统支持,老旧系统可能不支持 所有支持 IPv4 的系统都支持
性能 略慢(解析开销) 略快(无解析开销)

我是千寻, 这期内容到这里就结束了,我们有缘再会😂😂😂 !!!

昨天以前首页

面试官: “ 说一下怎么做到前端图片尺寸的响应式适配 ”

作者 千寻girling
2025年12月31日 15:26

前端开发中,图片的尺寸适配是响应式设计的核心部分之一,需要结合图片类型、容器场景、设备特性来选择方案。以下是常见的图片尺寸策略和多窗口适配方法:

一、先明确:前端常用的图片尺寸场景

不同场景下,图片的 “合适尺寸” 差异很大:

场景 建议尺寸范围 示例
图标 / 小图标 24×24 ~ 128×128(2 倍图) 按钮图标、头像缩略图
列表缩略图 300×200 ~ 600×400(2 倍图) 商品列表、文章封面缩略图
详情页主图 800×600 ~ 1920×1080(2 倍图) 商品详情图、Banner 图
背景图 1920×1080 ~ 3840×2160 全屏背景、页面 Banner
移动端适配图 750×1334(2 倍图)、1242×2208(3 倍图) 移动端页面元素图

二、多窗口适配的核心方法

1. 基础适配:max-width: 100%(通用)

最常用的适配方式,让图片不超过容器宽度,自动缩放高度:

img {
  max-width: 100%; /* 图片宽度不超过父容器 */
  height: auto;    /* 高度自动按比例缩放,避免变形 */
}

✅ 适用场景:大部分内联图片、列表图、详情图。

2. 背景图适配:background-size

针对背景图,通过 CSS 属性控制缩放逻辑:

.bg-img {
  width: 100%;
  height: 300px;
  background: url("bg.jpg") center/cover no-repeat; 
  /* 或单独设置: */
  background-size: cover; /* 覆盖容器,可能裁剪 */
  /* background-size: contain; 完整显示,可能留白 */
}
  • cover:优先覆盖容器,保持比例(常用全屏背景);
  • contain:优先完整显示,保持比例(常用图标背景)。

3. 响应式图片:srcset + sizes(精准加载)

让浏览器根据设备尺寸 / 像素比,自动选择合适的图片(减少加载体积):

<img 
  src="img-800.jpg"  <!-- 默认图 -->
  srcset="
    img-400.jpg 400w,  <!-- 400px宽的图 -->
    img-800.jpg 800w,  <!-- 800px宽的图 -->
    img-1200.jpg 1200w <!-- 1200px宽的图 -->
  "
  sizes="(max-width: 600px) 400px, 800px" <!-- 告诉浏览器容器宽度 -->
  alt="响应式图片"
>

✅ 适用场景:对加载性能要求高的大图(如 Banner、详情主图)。

4. 移动端高清图:2 倍图 / 3 倍图

针对 Retina 屏,提供高分辨率图,避免模糊:

<!-- 方法1:srcset 按像素比适配 -->
<img 
  src="img@2x.png" 
  srcset="
    img@1x.png 1x,  <!-- 普通屏 -->
    img@2x.png 2x,  <!-- Retina屏 -->
    img@3x.png 3x   <!-- 超高清屏 -->
  "
  alt="高清图"
>

<!-- 方法2:CSS 背景图(针对图标) -->
.icon {
  background: url("icon@2x.png") no-repeat;
  background-size: 24px 24px; /* 实际显示尺寸是24×24,图片是48×48 */
  width: 24px;
  height: 24px;
}

5. 容器限制:object-fit(控制图片在容器内的显示方式)

当图片宽高比与容器不一致时,避免变形:

.img-container {
  width: 300px;
  height: 300px;
  overflow: hidden;
}
.img-container img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* 覆盖容器,裁剪多余部分(常用头像、卡片图) */
  /* object-fit: contain; 完整显示,留白 */
  /* object-fit: fill; 拉伸变形(不推荐) */
}

6. 媒体查询:针对特定窗口尺寸切换图片

强制在不同屏幕下使用不同图片(适合差异较大的场景):

/* 移动端用小图 */
@media (max-width: 768px) {
  .banner {
    background-image: url("banner-mobile.jpg");
  }
}
/* 桌面端用大图 */
@media (min-width: 769px) {
  .banner {
    background-image: url("banner-desktop.jpg");
  }
}

三、总结适配思路

  1. 优先用 max-width: 100% + height: auto:覆盖 80% 的基础场景;
  2. 背景图用 background-size: cover/contain
  3. 大图用 srcset + sizes:兼顾性能和清晰度;
  4. 固定容器用 object-fit:避免图片变形;
  5. 移动端用 2 倍 / 3 倍图:保证高清显示。

面试官: “ 请你讲一下 package.json 文件 ? ”

作者 千寻girling
2025年12月30日 23:49

1. package.json 的作用

package.json 是 Node.js/npm 项目的核心配置文件,位于项目根目录,它的作用包括:

  • 描述项目信息:名称、版本、作者、许可证等。
  • 声明依赖:项目运行所需的包(dependencies)和开发所需的包(devDependencies)。
  • 定义脚本命令:通过 scripts 字段,让你可以用 npm run 执行自定义任务(如启动、测试、构建)。
  • 指定元数据:比如入口文件、浏览器兼容性等。

2. 基本结构示例

一个典型的 package.json 可能如下:

{
  "name": "my-project",
  "version": "1.0.0",
  "description": "A sample Node.js project",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest",
    "build": "webpack"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.7.0",
    "webpack": "^5.89.0"
  },
  "author": "Your Name",
  "license": "MIT",
  "keywords": ["node", "express", "example"]
}

3. 核心字段说明

3.1 项目信息字段

  • name:项目名称(必须小写,无空格)。
  • version:项目版本,遵循 SemVer(语义化版本),格式为 x.y.z(主版本。次版本。补丁版本)。
  • description:项目的简短描述。
  • author:作者信息,可以是字符串或对象(如 {"name": "xxx", "email": "xxx"})。
  • license:开源许可证类型(如 MITISCGPL)。
  • keywords:项目关键字数组,方便在 npm 上搜索。

3.2 入口与配置字段

  • main:指定项目的入口文件(默认是 index.js)。

  • type:指定模块系统类型:

    • "commonjs"(默认):使用 require() 导入。
    • "module":使用 import/export 语法。
  • files:发布到 npm 时需要包含的文件或目录。

  • repository:项目代码仓库地址。


3.3 依赖字段

  • dependencies:生产环境依赖(项目运行时必需的包),例如:

    "dependencies": {
      "react": "^18.2.0"
    }
    

    版本号前的 ^ 表示兼容当前版本的次版本更新。

  • devDependencies:开发环境依赖(仅开发时使用,比如测试、构建工具),例如:

    "devDependencies": {
      "eslint": "^8.55.0"
    }
    
  • peerDependencies:声明项目运行时需要的外部依赖版本(常用于插件或库)。

  • optionalDependencies:可选依赖,即使安装失败也不会影响项目。


3.4 脚本字段

  • scripts:定义可执行的命令,例如:

    "scripts": {
      "start": "node index.js",
      "dev": "nodemon index.js"
    }
    

    执行方法:

    npm run start
    npm run dev
    

4. package.json 的生成方式

  • 手动创建:直接新建 package.json 文件并写入内容。

  • 使用命令:

    npm init
    

    会通过交互方式生成。

  • 使用默认配置:

    npm init -y
    

    直接生成一个默认的 package.json


5. 与 package-lock.json 的关系

  • package.json:声明依赖的版本范围
  • package-lock.json:锁定安装时的具体版本,确保每次安装的依赖版本一致。

✅ 总结package.json 是项目的 “身份证” 和 “说明书”,它定义了项目的基本信息、依赖关系、可执行脚本等。掌握它的结构和字段,是使用 npm 和 Node.js 开发的基础。

面试官: “ 说一下你对 Cookie 的理解 ? ”

作者 千寻girling
2025年12月27日 19:32

1. 什么是 Cookie

Cookie(小甜饼)是 服务器发送给浏览器并保存在客户端的一小段数据,用于:

  • 记录用户状态(如登录信息、购物车内容)
  • 跟踪用户行为(如浏览历史、广告推送)
  • 存储少量配置信息(如主题偏好、语言设置)

特点:

  • 大小限制:通常每个 Cookie 最大 4KB
  • 数量限制:每个域名一般最多 50 个 Cookie
  • 自动携带:浏览器在访问同一域名时会自动将 Cookie 附加在请求头中发送给服务器

2. Cookie 的结构

一个 Cookie 通常包含以下字段:

字段名 说明
Name Cookie 的名称
Value Cookie 的值(通常经过 URL 编码)
Domain 可以访问该 Cookie 的域名
Path 可以访问该 Cookie 的路径
Expires / Max-Age Cookie 的过期时间(Expires 是具体日期,Max-Age 是秒数)
HttpOnly 如果设置,Cookie 不能通过 JavaScript 访问(防止 XSS 攻击)
Secure 如果设置,Cookie 只能通过 HTTPS 传输
SameSite 控制跨站请求时是否发送 Cookie(防止 CSRF 攻击)

3. Cookie 的工作流程

  1. 服务器发送 Cookie : 当浏览器第一次访问服务器时,服务器在响应头中添加:

    Set-Cookie: username=Tom; Path=/; HttpOnly; Secure
    

    浏览器收到后会将该 Cookie 保存到本地。

  2. 浏览器存储 Cookie: Cookie 会被保存在浏览器的某个文件或内存中,根据 Domain 和 Path 来区分。

  3. 浏览器发送请求时携带 Cookie : 之后每次访问同一域名和路径时,浏览器会自动在请求头中添加:

    Cookie: username=Tom
    

    服务器通过读取这个 Cookie 来识别用户。


4. Cookie 的分类

(1)会话 Cookie(Session Cookie)

  • 没有设置 Expires 或 Max-Age
  • 浏览器关闭后自动删除
  • 常用于保存短期会话信息(如登录状态)

(2)持久 Cookie(Persistent Cookie)

  • 设置了 Expires 或 Max-Age
  • 在过期时间前一直有效,即使浏览器关闭
  • 常用于保存长期信息(如记住登录、用户偏好)

5. Cookie 的优缺点

优点

  • 简单易用:服务器和浏览器都原生支持
  • 自动携带:无需手动处理
  • 轻量级:适合存储少量数据

缺点

  • 容量小:每个 Cookie 最大 4KB

  • 安全性差

    • 容易被窃取(XSS 攻击)
    • 容易被伪造(CSRF 攻击)
  • 性能影响

    • 每次请求都会携带,增加带宽消耗
    • 过多 Cookie 会影响页面加载速度

6. Cookie 的安全设置

为了提高安全性,建议设置以下属性:

  • HttpOnly:防止 JavaScript 访问(减少 XSS 风险)

  • Secure:只在 HTTPS 连接中传输

  • SameSite

    • Strict:仅在同站请求时发送
    • Lax:允许部分跨站请求(如 GET 表单提交)
  • 合理的过期时间:短期 Cookie 减少被盗风险


7. 示例:使用 Cookie

(1)服务器设置 Cookie(Node.js + Express)

const express = require('express');
const app = express();

app.get('/login', (req, res) => {
  // 设置一个会话 Cookie
  res.cookie('username', 'Tom', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'Strict',
    maxAge: 3600000 // 1 小时
  });
  res.send('登录成功');
});

app.get('/profile', (req, res) => {
  // 读取 Cookie
  const username = req.cookies.username;
  if (username) {
    res.send(`欢迎你,${username}`);
  } else {
    res.send('请先登录');
  }
});

app.listen(3000);

(2)浏览器查看 Cookie

  • Chrome:F12 → Application → Cookies
  • Firefox:F12 → Storage → Cookies

8. Cookie 与 Token 的区别

特性 Cookie Token
存储位置 浏览器 浏览器(LocalStorage/SessionStorage)或 App
传输方式 自动在 HTTP 请求头中发送 需要手动在请求头中添加(如 Authorization: Bearer <token>
容量限制 每个 Cookie 最大 4KB 无固定限制(但过大影响性能)
安全性 较低(易受 XSS/CSRF 攻击) 较高(可结合 HttpOnly、Secure、SameSite)
适用场景 简单会话管理、短期状态存储 复杂认证授权、跨域请求、移动应用

✅ 总结

  • Cookie 是服务器保存在浏览器的一小段数据,用于记录状态和跟踪用户
  • 自动携带在请求头中,方便但有容量和安全限制
  • 常用于简单会话管理,现代 Web 开发中更多与 Token 结合使用
  • 安全使用需设置 HttpOnlySecureSameSite 等属性

面试官: “ 说一下 JS 中什么是事件循环 ? ”

作者 千寻girling
2025年12月27日 17:36

JS 中的事件循环原理以及异步执行过程

这些知识点对新手来说可能有点难,但是是必须迈过的坎,逃避是解决不了问题的,本篇文章旨在帮你彻底搞懂它们。


1. JS 是单线程的

我们都知道 JS 是单线程执行的(原因:我们不想并行地操作 DOM,DOM 树不是线程安全的,如果多线程,那会造成冲突)。

这里小说明一下:V8 是谷歌浏览器的 JS 执行引擎,在运行 JS 代码的时候,是以函数作为一个个帧(保存当前函数的执行环境)按代码的执行顺序压入执行栈(call stack)中,栈顶的函数先执行,执行完毕后弹出再执行下一个函数。其中堆是用来存放各种 JS 对象的。

image.png

假设浏览器就是上图的这种结构的话,执行同步代码是没什么问题的,如下

function foo() {
    bar()
    console.log('foo')
}
function bar() {
    baz()
    console.log('bar')
}
function baz() {
    console.log('baz')
}

foo()

我们定义了 foobarbaz 三个函数,然后调用 foo 函数,控制台输出的结果为:

baz
bar
foo

执行过程如下:

  1. 一个全局匿名函数最先执行(JS 的全局执行入口,之后的例子将忽略),遇到 foo 函数被调用,将 foo 函数压入执行栈。
  2. 执行 foo 函数,发现 foo 函数体中调用了 bar 函数,则将 bar 函数压入执行栈。
  3. 执行 bar 函数,发现 bar 函数体中调用了 baz 函数,又将 baz 函数压入执行栈。
  4. 执行 baz 函数,函数体中只有一条语句 console.log('baz'),执行,在控制台打印:baz,然后 baz 函数执行完毕弹出执行栈。
  5. 此时的栈顶为 bar 函数,bar 函数体中的 baz() 语句已经执行完,接着执行下一条语句(console.log('bar')),在控制台打印:bar,然后 bar 函数执行完毕弹出执行栈。
  6. 此时的栈顶为 foo 函数,foo 函数体中的 bar() 语句已经执行完,接着执行下一条语句(console.log('foo')),在控制台打印:foo,然后 foo 函数执行完毕弹出执行栈。
  7. 至此,执行栈为空,这一轮执行完毕。

动图展示

还是图直观点,以上步骤对应的执行流程图如下:

fc266fc5ceece50a1622961cf201eec5.gif

非动图 image.png


2. 事件循环(event loop)

  • 事件循环:JS 处理异步任务的机制,因单线程特性,通过循环读取任务队列实现非阻塞。

  • 过程:

    1. 执行同步代码(调用栈清空)。
    2. 执行所有微任务(Promise回调等),直到微任务队列清空。
    3. 执行一个宏任务(setTimeout等),然后回到步骤 2,循环往复。

我们改变一下代码 1, 如下是代码 2:

function foo() {
    bar()
    console.log('foo')
}
function bar() {
    baz()
    console.log('bar')
}
function baz() { 
    setTimeout(() => {
        console.log('setTimeout: 2s')
    }, 2000)
    console.log('baz') 
}

foo()

根据 1 中的假设,浏览器只由一个 JS 引擎构成的话,那么所有的代码必然同步执行(因为 JS 执行是单线程的,所以当前栈顶函数不管执行时间需要多久,执行栈中该函数下面的其他函数必须等它执行完弹出后才能执行(这就是代码被阻塞的意思)),执行到 baz 函数体中的 setTimeout 时应该等 2 秒,在控制台中输出 setTimeout: 2s,然后再输出:baz。所以我们期望的输出顺序应该是:setTimeout: 2s -> baz -> bar -> foo(这是错的)。

浏览器如果真这样设计的话,肯定是有问题的!遇到 AJAX 请求、setTimeout 等比较耗时的操作时,我们页面需要长时间等待,就被阻塞住啥也干不了,出现了页面 “假死”,这样绝对不是我们想要的结果。

实际当然并非我以为的那样,这里先重点提醒一下:JS 是单线程的,这一点也没错,但是浏览器中并不仅仅只是由一个 JS 引擎构成,它还包括其他的一些线程来处理别的事情。如下图 !

image.png

浏览器除了 JS 引擎(JS 执行线程,后面我们只关注 JS 引擎中的执行栈)以外,还有 Web APIs(浏览器提供的接口,这是在 JS 引擎以外的)线程、GUI 渲染线程等。JS 引擎在执行过程中,如果遇到相关的事件(DOM 操作、鼠标点击事件、滚轮事件、AJAX 请求、setTimeout 等),并不会因此阻塞,它会将这些事件移交给 Web APIs 线程处理,而自己则接着往下执行。Web APIs 则会按照一定的规则将这些事件放入一个任务队列(callback queue,也叫 task queue)中,当 JS 执行栈中的代码执行完毕以后,它就会去任务队列中获取一个事件回调放入执行栈中执行,然后如此往复,这就是所谓的事件循环机制。

线程名 作用
JS 引擎线程 也称为 JS 内核,负责处理 JavaScript 脚本。(例如 V8 引擎)负责解析 JS 脚本,运行代码。一直等待着任务队列中的任务的到来,然后加以处理。一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程运行 JS 程序。
事件触发线程 归属于渲染进程而不是 JS 引擎,用来控制事件循环。当 JS 引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、Ajax 异步请求等),会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。 注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都是排队等待 JS 引擎处理,JS 引擎空闲时才会执行。
定时触发器线程 setIntervalsetTimeout所在的线程。浏览器定时计数器并不是由 JS 引擎计数的。JS 引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确,因此,通过单独的线程来计时并触发定时。计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行。注意:W3C 在 HTML 标准中规定,setTimeout中低于 4ms 的时间间隔算为 4ms。
异步 HTTP 请求线程 XMLHttpRequest在连接后通过浏览器新开一个线程请求。当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,再由 JS 引擎执行。
GUI 渲染线程 负责渲染浏览器界面,包括:解析 HTML、CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。重绘(Repaint)以及回流(Reflow)处理。

这里让我们对事件循环先来做个小总结

  1. JS线程负责处理JS代码,当遇到一些异步操作的时候,则将这些异步事件移交给Web APIs 处理,自己则继续往下执行。
  2. Web APIs线程将接收到的事件按照一定规则按顺序添加到任务队列中(应该是添加到任务集合中的各个事件队列中)。
  3. JS线程处理完当前的所有任务以后(执行栈为空),它会去查看任务队列中是否有等待被处理的事件,若有,则取出一个事件回调放入执行栈中执行。
  4. 然后不断循环第3步。

让我们来看看真正的浏览器中执行是什么个流程吧!

动图展示

第二段代码 3.gif

细心的小伙伴可能有发现Web API在计时器时间到达后将匿名回调函数添加到任务队列中了,虽然定时器时间已到,但它目前并不能执行!!!因为JS的执行栈此时并非空,必须要等到当前执行栈为空后才有机会被召回到执行栈执行。由此,我们可以得出一个结论:setTimeout设置的时间其实只是最小延迟时间,而并不是确切的等待时间。(当主线程的任务耗时比较长的时候,等待时间将会变得更长


3. 事件循环(进阶)与异步

3.1 试试 setTimeout(fn, 0)

function foo() {
    console.log('foo')
}

setTimeout(function() {
    console.log('setTimeout: 0s')
}, 0);

foo();

运行结果:

foo
setTimeout: 0s
4.gif

即使 setTimeout 的延时设置为 0(实际上最小延时 >= 4ms),JS 执行栈也将该延时事件发放给 Web API 处理,Web API 再将事件添加到任务队列中,等 JS 执行栈为空时,该延时事件再压入执行栈中执行。


3.2 事件循环中的 Promise

其实以上的浏览器模型是ES5标准的,ES6+标准中的任务队列在此基础上新增了一种,变成了如下两种:

3.2.1 宏任务 / 微任务

现在W3C重新对事件循环进行了定义,取消了宏任务,取而代之的是任务队列,微任务依旧保留,优先级为最高。

MDN 官网 : 事件循环会将作业分成两类: 任务微任务。微任务具有更高的优先级,在任务队列被拉出之前,微任务队列会先被排空

任务队列(macrotask queue)普通优先级的任务,通常包括:

  • setTimeout / setInterval / setImmediate(Node.js)
  • I/O 操作(文件读写、Ajax事件 / 网络请求等)
  • UI 渲染事件 (用户交互事件)
  • 脚本整体代码(第一次执行的同步代码)

微任务队列(microtask queue)高优先级的任务,通常包括:

  • Promise.then / Promise.catch / Promise.finally
  • async/await 中 await 后面的代码(其实是 .then 的语法糖)
  • MutationObserver(浏览器)
  • process.nextTick(Node.js,优先级比普通微任务更高)
image.png

事件循环的处理流程变成了如下:

  1. JS 线程负责处理 JS 代码,当遇到一些异步操作的时候,则将这些异步事件移交给 Web APIs 处理,自己则继续往下执行。
  2. Web APIs 线程将接收到的事件按照一定规则添加到任务队列中,宏事件添加到宏任务队列中,微事件添加到微事件队列中。
  3. JS 线程处理完当前的所有任务以后(执行栈为空),它会先去微任务队列获取事件,并将微任务队列中的所有事件一件件执行完毕,直到微任务队列为空后再去宏任务队列中取出一个事件执行。
  4. 然后不断循环第 3 步。
image.png

排一下先后顺序: 执行栈 --> 微任务 --> 渲染 --> 下一个宏任务


3.2.2 单独使用 Promise

function foo() {
    console.log('foo')
}

console.log('global start')

new Promise((resolve) => {
    console.log('promise')
    resolve()
}).then(() => {
    console.log('promise then')
})

foo()

console.log('global end')

控制台输出的结果为:

global start
promise
foo
global end
promise then

动图展示

5.gif

代码执行过程解析(文字描述)

  1. 执行同步代码
  • 执行 console.log('global start'),控制台输出:global start
  1. 执行 new Promise(...)
  • 注意:在使用 new 关键字创建 Promise 对象时,传递给 Promise 的函数称为 executor

    • 当 Promise 被创建时,executor 函数会自动同步执行
    • .then 里的回调才是异步执行的部分。
  • 执行 Promise 参数中的匿名函数(同步执行):

    • 执行 console.log('promise'),控制台输出:promise

    • 执行 resolve(),将 Promise 状态变为 resolved

  • 继续执行 .then(...)

    • 遇到 .then 会将回调提交给 Web API 处理。
    • Web API 将该回调添加到 微任务队列(此时微任务队列中有一个 Promise 事件待执行)。
  1. 继续执行同步代码
  • 执行栈在提交完 Promise 事件后,继续往下执行:

    • 执行 foo() 函数,控制台输出:foo

    • 执行 console.log('global end'),控制台输出:global end

  • 至此,本轮事件循环的同步代码执行完毕,执行栈为空。

  1. 处理微任务队列
  • 事件循环机制首先查看 微任务队列 是否为空:

    • 发现有一个 Promise 事件待执行,将其压入执行栈。

    • 执行 .then 中的回调:

      • 执行 console.log('promise then'),控制台输出:promise then
    • 至此,新的一轮事件循环(Promise 事件)执行完毕,执行栈为空。

  1. 检查任务队列
  • 执行栈再次为空,事件循环:

    1. 先查看 微任务队列,发现已空。
    2. 再查看 宏任务队列,发现也为空。
  • 执行栈进入等待事件状态。


3.2.3 Promise 结合 setTimeout

function foo() {
    console.log('foo')
}

console.log('global start')

setTimeout(() => {
    console.log('setTimeout: 0s')
}, 0)

new Promise((resolve) => {
    console.log('promise')
    resolve()
}).then(() => {
    console.log('promise then')
})

foo()

console.log('global end')

控制台输出的结果为:

global start
promise
foo
global end
promise then
setTimeout: 0s

动图展示

6.gif
  1. 执行同步代码
  • 执行 console.log('global start'),控制台输出: global start
  1. 处理 setTimeout(改变部分)
  • 继续往下执行,遇到 setTimeout

    • JS 执行栈将其移交给 Web API 处理。
    • 延迟 0 秒后,Web API 将 setTimeout 事件添加到 宏任务队列(此时宏任务队列中有一个 setTimeout 事件待处理)。
  1. 继续执行同步代码
  • JS 线程转交 setTimeout 事件后,继续往下执行:

    • 遇到 new Promise(...)

      • Promise 的 executor 函数 同步执行

        • 执行 console.log('promise'),控制台输出:promise

        • 执行 resolve(),将 Promise 状态变为 resolved

      • 执行 .then(...)

        • 遇到 .then 会将回调提交给 Web API 处理。
        • Web API 将该回调添加到 微任务队列(此时微任务队列中有一个 Promise 事件待处理)。
  1. 继续执行同步代码
  • 执行栈在提交完 Promise 事件后,继续往下执行:

    • 执行 foo() 函数,控制台输出:foo

    • 执行 console.log('global end'),控制台输出:global end

  • 至此,本轮事件循环的同步代码执行完毕,执行栈为空。

  1. 处理微任务队列
  • 事件循环机制首先查看 微任务队列 是否为空:

    • 发现有一个 Promise 事件待执行,将其压入执行栈。

    • 执行 .then 中的回调:

      • 执行 console.log('promise then'),控制台输出: promise then
    • 至此,新的一轮事件循环(Promise 事件)执行完毕,执行栈为空。

  1. 处理宏任务队列(改变部分)
  • 执行栈再次为空,事件循环:

    1. 先查看 微任务队列,发现已空。

    2. 再查看 宏任务队列,发现有一个 setTimeout 事件待处理:

    • 将 setTimeout 中的匿名函数压入执行栈执行:

      • 执行 console.log('setTimeout: 0s'),控制台输出:setTimeout: 0s
    • 至此,新的一轮事件循环(setTimeout 事件)执行完毕,执行栈为空。

7.** 检查任务队列**

  • 执行栈再次为空,事件循环:

    1. 先查看 微任务队列,发现已空。
    2. 再查看 宏任务队列,发现也为空。
  • 执行栈进入等待事件状态。


3.3 事件循环中的 async/await

这里简单介绍下async函数:

  1. 函数前面 async 关键字的作用就2点:①这个函数总是返回一个promise。②允许函数内使用await关键字。

  2. 关键字 await 使 async 函数一直等待(执行栈当然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await只在async函数函数里面奏效

  3. async函数只是一种比promise更优雅得获取promise结果(promise链式调用时)的一种语法而已。

function foo() {
    console.log('foo')
}

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2() {
    console.log('async2')
}

console.log('global start')
async1()
foo()
console.log('global end')

执行的结果如下:

global start
async1 start
async2
foo
global end
async1 end
  1. 执行同步代码
  • 执行 console.log('global start'),控制台输出:global start
  1. 调用 async1()
  • 执行 async1(),进入 async1 函数体:

    • 执行 console.log('async1 start'),控制台输出:async1 start

    • 执行 await async2()

      • await 关键字会暂停 async1 函数的执行,直到 await 后面的 Promise 返回结果。
      • await async2() 会像调用普通函数一样执行 async2()
  1. 执行 async2()
  • 进入 async2 函数体:

    • 执行 console.log('async2'),控制台输出:async2

    • async2 函数执行结束,弹出执行栈。

    • 由于 async2 没有显式返回 Promise,它会隐式返回一个已 resolved 的 Promise。

4.暂停 async1() 并继续执行同步代码

  • 因为 await 关键字之后的代码被暂停,async1 函数执行结束,弹出执行栈。

  • JS 主线程继续向下执行:

    • 执行 foo() 函数,控制台输出:foo

    • 执行 console.log('global end'),控制台输出:global end

  • 至此,本轮事件循环的同步代码执行完毕,执行栈为空。

  1. 事件循环处理微任务
  • 事件循环机制开始工作:

    1. 先查看 微任务队列

      • 发现有一个微任务事件,该事件是 async1 函数中 await async2() 之后的代码(可以理解为:用一个匿名函数包裹 await 之后的代码,作为微任务事件)。

      • 执行该微任务:

        • 执行 console.log('async1 end'),控制台输出:async1 end
    2. 执行栈再次为空,本轮事件执行结束。

  1. 检查任务队列
  • 事件循环机制再次查看:

    1. 微任务队列:已空。
    2. 宏任务队列:也为空。
  • 执行栈进入等待事件状态。


4. 大综合(自测)

4.1 简单融合

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

输出结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

4.2 变形 1

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

输出的结果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

4.3 变形 2

async function async1() {
    console.log('async1 start');
    await async2();
    setTimeout(function() {
        console.log('setTimeout1');
    },0)
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

输出的结果:

script start
async1 start
async2
promise1
script end
promise2
setTimeout3
setTimeout1

4.4 变形 3

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

输出的结果:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout

5. 结语

  • JS 是单线程执行的,同一时间只能处理一件事。
  • 浏览器是多线程的,JS 引擎通过分发这些耗时的异步事件给 Web APIs 线程处理,避免了单线程被阻塞。
  • 事件循环机制是为了协调事件、用户交互、JS 脚本、页面渲染、网络请求等事件的有序执行
  • 微任务的优先级高于宏任务

面试官: “ 请你说一下什么是 ajax ? ”

作者 千寻girling
2025年12月27日 13:33

一、AJAX 核心定义

AJAX 是 Asynchronous JavaScript and XML 的缩写,翻译为 “异步的 JavaScript 和 XML”。

  • 本质:它不是一种新的编程语言,而是一套使用现有技术组合实现的编程方案
  • 核心作用:让 JavaScript 在不刷新整个网页的情况下,异步地与服务器交换数据,实现网页局部更新。
  • 关键特点:异步(请求发送后,页面不用等待服务器响应,仍可正常交互)、局部更新(只更新需要变化的部分,提升用户体验)。

补充:虽然名字里有 XML,但现在实际开发中几乎都用JSON(更轻量、易解析)来传输数据,AJAX 只是沿用了历史名称。

二、AJAX 工作原理

AJAX 的核心是浏览器提供的 XMLHttpRequest 对象(简称 XHR),现代浏览器也提供了更易用的 fetch API。其基本工作流程如下:

  1. 创建 AJAX 请求对象(XHR/fetch);
  2. 配置请求参数(请求方式、URL、是否异步等);
  3. 发送请求到服务器;
  4. 监听服务器响应状态;
  5. 接收服务器返回的数据;
  6. 用 JavaScript 更新网页局部内容。

三、AJAX 代码示例

1. 传统 XHR 方式(兼容所有浏览器)

// 1. 创建XHR对象
const xhr = new XMLHttpRequest();

// 2. 配置请求:请求方式(GET)、URL、是否异步(true)
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);

// 3. 监听请求状态变化(核心)
xhr.onreadystatechange = function() {
    // readyState=4 表示请求完成;status=200 表示响应成功
    if (xhr.readyState === 4 && xhr.status === 200) {
        // 4. 解析服务器返回的JSON数据
        const data = JSON.parse(xhr.responseText);
        // 5. 局部更新页面(比如把数据显示到id为result的元素里)
        document.getElementById('result').innerHTML = `
            <h3>任务标题:${data.title}</h3>
            <p>是否完成:${data.completed ? '是' : '否'}</p>
        `;
    }
};

// 处理请求失败的情况
xhr.onerror = function() {
    document.getElementById('result').innerHTML = '请求失败!';
};

// 4. 发送请求
xhr.send();

2. 现代 fetch 方式(ES6+,更简洁)

fetch 是 AJAX 的现代替代方案,基于 Promise,语法更优雅:

// 1. 发送GET请求
fetch('https://jsonplaceholder.typicode.com/todos/1')
    // 2. 处理响应:先判断是否成功,再解析为JSON
    .then(response => {
        if (!response.ok) {
            throw new Error('请求失败,状态码:' + response.status);
        }
        return response.json();
    })
    // 3. 使用数据更新页面
    .then(data => {
        document.getElementById('result').innerHTML = `
            <h3>任务标题:${data.title}</h3>
            <p>是否完成:${data.completed ? '是' : '否'}</p>
        `;
    })
    // 4. 捕获异常
    .catch(error => {
        document.getElementById('result').innerHTML = error.message;
    });

四、AJAX 的典型应用场景

  • 表单提交(比如登录验证,不用刷新页面就能提示 “用户名密码错误”);
  • 数据分页加载(比如滚动到底部自动加载下一页内容);
  • 搜索框联想(输入关键词实时显示匹配结果);
  • 局部数据刷新(比如网页的点赞、评论功能,点击后直接更新数字)。

五、关键注意点

  1. 同源策略:浏览器默认限制 AJAX 请求只能访问同域名、同端口、同协议的服务器(比如http://localhost:8080不能请求http://baidu.com),跨域需要服务器配置 CORS 或使用代理。
  2. 异步特性:AJAX 请求是异步的,不能在请求发送后立即获取结果,必须在回调函数(onreadystatechange)或 Promise 的then里处理返回数据。

总结

  1. AJAX 是一套实现 “网页异步请求数据、局部更新” 的技术方案,核心是XMLHttpRequest对象(或现代的fetch)。
  2. 核心优势:无需刷新整个页面,提升用户体验,实现网页与服务器的异步数据交互。
  3. 现代开发中,fetch(结合 Promise/async-await)已逐步替代传统 XHR,是 AJAX 的主流实现方式。
❌
❌