NodeJs
Node.js 面试的核心考察点集中在 异步编程模型、核心模块应用、性能优化 及 工程化实践 四个层面,以下是高频考点及解析:
一、核心概念与异步编程
1. 什么是 Node.js?它的核心特点是什么?
- 定义:Node.js 是基于 Chrome V8 引擎的 JavaScript 运行时环境,允许在服务器端执行 JS 代码。
- 核心特点:
- 单线程 + 事件循环:避免多线程上下文切换开销,通过非阻塞 I/O 处理高并发。
- 非阻塞 I/O:I/O 操作(如文件读写、网络请求)不阻塞主线程,通过回调/ Promise 通知结果。
- 跨平台:支持 Windows、Linux、macOS,依赖 libuv 库实现底层系统调用兼容。
- npm 生态:拥有全球最大的开源包管理系统,丰富的第三方库(如 Express、Koa)。
2. Node.js 的事件循环(Event Loop)机制?
事件循环是 Node.js 实现异步非阻塞的核心,负责调度回调函数的执行。6 个阶段依次执行,每个阶段有一个回调队列:
- timers:执行
setTimeout/setInterval回调(按延迟时间排序)。 - pending callbacks:执行延迟到下一轮的 I/O 回调(如 TCP 错误回调)。
- idle, prepare:内部使用,开发者无需关注。
- poll(轮询):
- 若有已完成的 I/O 事件(如文件读取、网络响应),执行其回调。
- 若无回调且存在
setImmediate回调,进入下一阶段;否则阻塞等待 I/O。
- check:执行
setImmediate回调(在 poll 阶段结束后立即执行)。 - close callbacks:执行关闭事件回调(如
socket.on('close', ...))。
示例:timers vs setImmediate
// 场景 1:主模块中,两者执行顺序不确定(取决于系统调度)
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));// 场景 2:I/O 回调中,setImmediate 一定先执行(poll 阶段结束后进入 check 阶段)
fs.readFile(__filename, () => {setTimeout(() => console.log('timeout'), 0);setImmediate(() => console.log('immediate')); // 先执行
});
3. 回调地狱(Callback Hell)是什么?如何解决?
- 定义:多层嵌套回调函数导致代码可读性差、维护困难的问题(如嵌套的
fs.readFile或网络请求)。 - 解决方式:
- Promise 封装:将回调转为 Promise,通过
.then()链式调用。const readFile = (path) => new Promise((resolve, reject) => {fs.readFile(path, (err, data) => err ? reject(err) : resolve(data)); }); readFile('a.txt').then(data => readFile('b.txt')).then(data => console.log(data)); - async/await:基于 Promise 的语法糖,将异步代码写得像同步代码。
const readFiles = async () => {const aData = await readFile('a.txt');const bData = await readFile('b.txt');console.log(aData, bData); }; - 工具库:使用
async.js等库的series/parallel方法管理异步流程。
- Promise 封装:将回调转为 Promise,通过
4. Promise 的三种状态?如何使用?
- 状态:
pending:初始状态,未完成或失败。fulfilled:操作成功完成,触发.then()回调。rejected:操作失败,触发.catch()回调。
- 核心特点:状态一旦改变(
pending → fulfilled或pending → rejected),则不可再变。 - 示例:
const promise = new Promise((resolve, reject) => {setTimeout(() => {const random = Math.random();random > 0.5 ? resolve('成功') : reject(new Error('失败'));}, 1000); }); promise.then(res => console.log(res)).catch(err => console.error(err));
二、核心模块与 API
1. 常用的核心模块有哪些?各自作用?
| 模块名 | 核心作用 |
|---|---|
fs | 文件系统操作(读/写文件、创建目录等),支持同步(fs.readFileSync)和异步(fs.readFile)。 |
path | 处理文件路径(如 path.join 拼接路径、path.resolve 转为绝对路径),兼容不同系统。 |
http | 构建 HTTP 服务器(http.createServer)和客户端(http.request),实现网络通信。 |
url | 解析 URL 字符串(如 url.parse 提取 query、pathname),Node.js 11+ 推荐用 new URL()。 |
events | 事件触发器(EventEmitter 类),实现自定义事件(如 on 监听、emit 触发)。 |
stream | 流处理(如文件流、网络流),支持分块读取大文件,减少内存占用。 |
2. fs 模块的同步与异步方法区别?如何选择?
- 异步方法(如
fs.readFile):- 非阻塞,不会卡住主线程,适合 I/O 密集型场景(如服务器处理多个请求)。
- 需通过回调或 Promise 处理结果,代码稍复杂。
- 同步方法(如
fs.readFileSync):- 阻塞主线程,执行时其他代码无法运行,适合初始化阶段(如读取配置文件)。
- 代码简洁,直接返回结果,出错需用
try/catch捕获。
- 选择原则:服务器运行中优先用异步,初始化或简单脚本可用同步。
3. 什么是 Stream(流)?有哪些类型?应用场景?
- 定义:Stream 是处理大文件或持续数据(如网络流)的抽象接口,将数据分块(
chunk)读取/写入,避免一次性加载全部数据到内存。 - 核心类型:
- Readable:可读流(如
fs.createReadStream读取文件)。 - Writable:可写流(如
fs.createWriteStream写入文件)。 - Duplex:双向流(可读可写,如
net.Socket网络套接字)。 - Transform:转换流(修改数据,如
zlib.createGzip压缩数据)。
- Readable:可读流(如
- 应用场景:大文件复制、日志写入、HTTP 响应传输、数据压缩等。
- 示例:文件复制:
const readStream = fs.createReadStream('source.txt'); const writeStream = fs.createWriteStream('target.txt'); readStream.pipe(writeStream); // 管道:自动将可读流数据写入可写流
4. EventEmitter 是什么?如何使用?
- 定义:Node.js 事件驱动的核心类(来自
events模块),实现了观察者模式,支持自定义事件的监听和触发。 - 核心方法:
on(eventName, listener):监听事件,可多次触发。once(eventName, listener):监听事件,仅触发一次。emit(eventName, ...args):触发事件,传递参数给监听器。off(eventName, listener):移除事件监听。
- 示例:
const { EventEmitter } = require('events'); const emitter = new EventEmitter();// 监听 'message' 事件 emitter.on('message', (data) => console.log('收到消息:', data));// 触发 'message' 事件 emitter.emit('message', 'Hello Node.js'); // 输出:收到消息:Hello Node.js
三、性能优化与并发处理
1. Node.js 适合什么场景?不适合什么场景?
- 适合场景:
- I/O 密集型:如 API 服务、数据库查询、文件读写(非阻塞 I/O 高效处理并发请求)。
- 实时通信:如聊天室、WebSocket 服务(事件循环适合处理高频消息)。
- 数据流处理:如日志分析、文件压缩(Stream 分块处理)。
- 不适合场景:
- CPU 密集型:如大规模计算、视频编码(单线程会阻塞事件循环,导致其他请求排队)。
- 强实时性要求:如游戏服务器(事件循环存在延迟,无法保证毫秒级响应)。
2. 如何解决 Node.js 的 CPU 密集型任务问题?
- 1. 子进程(Child Process):通过
child_process模块创建子进程,将 CPU 密集任务交给子进程执行,避免阻塞主进程。const { fork } = require('child_process'); const child = fork('cpu-task.js'); // 子进程执行 CPU 密集任务 child.send({ data: '任务数据' }); // 主进程向子进程发送数据 child.on('message', (result) => console.log('任务结果:', result)); // 接收子进程结果 - 2. 集群(Cluster):利用多核 CPU,通过
cluster模块创建多个工作进程(Worker),主进程(Master)负责分发请求。const cluster = require('cluster'); const numCPUs = require('os').cpus().length;if (cluster.isPrimary) {// 主进程:创建工作进程for (let i = 0; i < numCPUs; i++) cluster.fork(); } else {// 工作进程:启动 HTTP 服务器http.createServer((req, res) => {res.end('Hello from Worker ' + process.pid);}).listen(3000); } - 3. 第三方服务:将 CPU 密集任务(如视频编码)交给专门的服务(如 FFmpeg 服务)处理,Node.js 仅负责调度。
3. 如何优化 Node.js 应用性能?
- 1. 代码层面:
- 避免同步 I/O:优先用异步方法,防止阻塞事件循环。
- 复用资源:如数据库连接池(避免频繁创建/关闭连接)、缓存(Redis 缓存热点数据)。
- 优化循环:减少循环内的复杂操作,避免嵌套循环。
- 2. 网络层面:
- 启用 HTTP 长连接(
keep-alive):减少 TCP 连接建立/关闭开销。 - 压缩响应:用
compression中间件开启 Gzip/Brotli 压缩。 - 合理使用缓存:设置
Cache-Control头,缓存静态资源。
- 启用 HTTP 长连接(
- 3. 工具层面:
- 性能分析:用
node --inspect或clinic.js分析事件循环延迟、内存泄漏。 - 进程管理:用
PM2管理 Node.js 进程(自动重启、负载均衡)。
- 性能分析:用
4. 什么是内存泄漏?Node.js 中常见的内存泄漏场景?
- 定义:程序中已不再使用的内存未被释放,导致内存占用持续增长,最终引发进程崩溃。
- 常见场景:
- 未移除的事件监听:如
EventEmitter绑定的on事件未用off移除,导致监听器累积。// 错误示例:每次请求都添加新监听,未移除 app.get('/', (req, res) => {emitter.on('data', (data) => res.send(data)); }); - 全局变量:意外创建的全局变量(如未声明的变量)不会被垃圾回收(GC)。
- 闭包引用:闭包长期持有外部变量(如缓存对象未清理)。
- 未关闭的资源:如数据库连接、文件句柄未关闭,导致资源句柄泄漏。
- 未移除的事件监听:如
- 排查工具:用
node --expose-gc手动触发 GC,结合Chrome DevTools分析内存快照。
四、Web 框架与工程化
1. Express 与 Koa 的区别?
两者均为 Node.js 主流 Web 框架,核心区别在于中间件机制和异步支持:
| 维度 | Express | Koa(由 Express 原团队开发) |
|---|---|---|
| 中间件机制 | 基于回调函数的“线性”中间件(app.use 按顺序执行)。 | 基于 async/await 的“洋葱模型”中间件,支持异步流程控制。 |
| 异步支持 | 原生支持回调,需手动封装 Promise 或用第三方库。 | 原生支持 async/await,异步代码更简洁。 |
| 核心特性 | 内置路由、模板引擎等,功能完整,开箱即用。 | 核心更轻量(无内置路由),需通过中间件扩展(如 koa-router)。 |
| 错误处理 | 需在每个中间件中处理错误,或用全局错误中间件。 | 洋葱模型中,错误可通过 try/catch 统一捕获(更优雅)。 |
Koa 洋葱模型示例:
const Koa = require('koa');
const app = new Koa();// 中间件 1
app.use(async (ctx, next) => {console.log('1. 进入中间件 1');await next(); // 执行下一个中间件console.log('4. 离开中间件 1');
});// 中间件 2
app.use(async (ctx, next) => {console.log('2. 进入中间件 2');await next();console.log('3. 离开中间件 2');
});app.listen(3000);
// 访问时输出顺序:1 → 2 → 3 → 4
2. 什么是中间件(Middleware)?作用是什么?
- 定义:中间件是处理 HTTP 请求的函数,在请求到达目标路由/响应返回客户端的过程中执行特定逻辑。
- 核心作用:
- 统一处理通用逻辑:如日志记录、身份验证、跨域处理、错误捕获。
- 修改请求/响应对象:如解析请求体(
express.json())、设置响应头。
- Express 中间件示例:
const express = require('express'); const app = express();// 日志中间件 app.use((req, res, next) => {console.log(`${req.method} ${req.url} - ${new Date()}`);next(); // 调用 next() 传递给下一个中间件 });// 路由中间件 app.get('/', (req, res) => {res.send('Hello Express'); });
3. 如何实现 Node.js 服务的热重载?
热重载(代码修改后自动重启服务)提升开发效率,常用方案:
- 1.
nodemon:监听文件变化,自动重启 Node.js 进程(开发环境常用)。npm install nodemon --save-dev # package.json 配置:"dev": "nodemon app.js" - 2.
PM2热重载:生产环境可用 PM2 的--watch选项,文件变化时重启进程。pm2 start app.js --watch - 3. 模块热替换(HMR):复杂应用可结合 Webpack 或 Vite 实现模块级热替换(无需重启进程)。
五、数据库与缓存
1. Node.js 中如何连接 MySQL 数据库?
- 常用库:
mysql2(支持 Promise 和 async/await,性能优于旧的mysql库)。 - 示例:
const mysql = require('mysql2/promise');// 创建连接池(推荐,避免频繁创建连接) const pool = mysql.createPool({host: 'localhost',user: 'root',password: '123456',database: 'test_db',waitForConnections: true,connectionLimit: 10, // 最大连接数 });// 执行查询 const queryData = async () => {const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [1]);console.log(rows); };
2. Redis 在 Node.js 中的应用场景?如何使用?
- 应用场景:
- 缓存:存储热点数据(如用户信息、商品列表),减少数据库查询。
- 会话存储:存储用户登录状态(如 Session ID)。
- 消息队列:通过
list类型实现简单的消息队列(如异步任务处理)。 - 计数器:如文章阅读量、接口请求次数(
incr命令原子性操作)。
- 使用示例(
ioredis库):const Redis = require('ioredis'); const redis = new Redis({ host: 'localhost', port: 6379 });// 设置缓存 await redis.set('user:1', JSON.stringify({ id: 1, name: 'Alice' }), 'EX', 3600); // 过期时间 1 小时// 获取缓存 const userStr = await redis.get('user:1'); const user = JSON.parse(userStr);// 计数器 await redis.incr('article:1:views'); // 阅读量 +1
结尾交付物提议
要不要我帮你整理一份 Node.js 高频面试题及答案汇总文档?包含核心概念、异步编程、核心模块、性能优化等六大模块,每个问题附考点解析和代码示例,方便你针对性复习。
