Node

1. Node.js 与 JavaScript 有什么不同?

Node.js 是⼀个⽤于构建可扩展⽹络应⽤的运⾏时环境,⽽ JavaScript 是⼀种⽤于在⽹⻚上进⾏交互的脚本语⾔。主要区别包括:

  • Node.js 是基于 V8 引擎构建的服务器端平台,允许使⽤ JavaScript 编写服务器端代码,⽽ JavaScript 通常⽤于在浏览器中运⾏。

  • Node.js 提供了⼀系列内置模块和 API,使得可以直接访问⽂件系统、⽹络以及操作系统功能等;⽽在浏览器端 JavaScript 不能直接进⾏这些操作。

2. 什么场景下使⽤ Node.js?优势是什么

Node.js 适合于以下场景:

  1. ⾼并发的 I/O 密集型应⽤:如实时聊天应⽤、在线游戏、消息推送等,其中⼤量并发请求需要快速响应。

  2. 数据流处理应⽤:对于需要处理⼤量数据流的应⽤程序,例如⽇志处理、实时数据监控和分析等。

  3. 单⻚⾯应⽤程序的后端服务:作为单⻚⾯应⽤程序(SPA)的后台服务,提供 API 和数据的⽀持。

Node.js 的优势:

  • ⾮阻塞 I/O:Node.js 采⽤事件驱动、⾮阻塞 I/O 模型,能够处理⼤量并发请求⽽不会因为等待 I/O 操作⽽阻塞。

  • 使⽤ JavaScript 全栈开发:前后端都可以使⽤ JavaScript 编程语⾔,避免了在前后端之间切换语⾔带来的不必要的复杂性。

  • 丰富的包管理⼯具 npm:Node.js ⽣态系统庞⼤,拥有丰富的第三⽅库和模块可供使⽤,⽽ npm 是⼀个强⼤的包管理⼯具,让开发者轻松地使⽤和分享代码。

3. EventEmitter 做了什么?实现思路

EventEmitter 是 Node.js 核⼼模块之⼀,⽤于处理事件的订阅和发布。它允许多个监听器订阅某个特定事件,并在该事件发⽣时异步地调⽤这些监听器。

实现思路:

  1. 创建⼀个事件触发器:内部维护⼀个事件名到监听器数组的映射。当新的监听器被添加或事件被触发时,相应的操作会更新这个映射关系。

  2. 注册事件监听器:使⽤ on ⽅法注册事件监听器,在特定事件发⽣时执⾏回调函数。

  3. 触发事件:使⽤emit ⽅法触发相应的事件,对应的监听器会被异步地调⽤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class EventEmitter {
constructor() {
this.events = {};
}

on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}

emit(eventName, ...args) {
const listeners = this.events[eventName];
if (listeners) {
for (const listener of listeners) {
listener(...args);
}
}
}
}

// 使⽤示例
const emitter = new EventEmitter();

emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});

emitter.emit('greet', 'John'); // 输出: Hello, John!

4. 事件循环是什么?

JavaScript 中的事件循环:在浏览器环境下,JavaScript 使⽤事件循环来处理异步操作。当有异步任务完成时,它会被放⼊⼀个事件队列中,然后按照顺序执⾏这些任务,并将结果返回给相应的回调函数。

Node.js 中的事件循环:Node.js 也具有类似的事件循环机制,但其实现⽅式略有不同。Node.js 的事件循环基于 libuv 库实现,采⽤单线程模型,通过轮询事件队列来实现异步 I/O 操作。它允许 Node.js 执⾏⾮阻塞 I/O 操作,从⽽提⾼了应⽤程序的并发能⼒和吞吐量。

  • 区别
  1. 实现机制:

    • JavaScript 在浏览器中使⽤浏览器的事件循环机制,由浏览器提供底层⽀持。
    • Node.js 使⽤ libuv 库实现事件循环,可以更好地控制系统资源和执⾏异步操作。
  2. 应⽤场景:

    • JavaScript 的事件循环主要⽤于处理浏览器中的异步任务,例如定时器、事件监听等。
    • Node.js 的事件循环适⽤于服务器端,能够处理⼤量的并发 I/O 操作,例如⽂件读写、⽹络通信等。
  3. ⾮阻塞 I/O:

    • Node.js 的事件循环允许执⾏⾮阻塞 I/O 操作,使得 Node.js 能够在等待 I/O 操作的同时继续执⾏其他任务。这是 Node.js 与浏览器 JavaScript 事件循环的重要区别之⼀。

5. 流是什么?

流(Stream)在 Node.js 中是⼀种处理⽂件、⽹络数据和其他 I/O 操作的抽象概念。它允许以⼀种⾼效的⽅式按顺序读取或写⼊数据,⽽⽆需将整个数据加载到内存中。

流可以分为可读流和可写流:

  1. 可读流:⽤于从数据源(例如⽂件、⽹络连接、标准输⼊等)读取数据。它允许逐块地读取数据,通常⽤于处理⼤量数据或连续的数据流。例如,在处理⼤型⽇志⽂件时,可读流可以逐⾏读取⽂件内容⽽不必⼀次性加载整个⽂件到内存中。

  2. 可写流:⽤于向⽬标位置(例如⽂件、⽹络连接、HTTP 响应等)写⼊数据。和可读流相反,可写流允许逐块地写⼊数据,这在需要持续产⽣输出的场景下⾮常有⽤,⽐如实时⽇志记录或 HTTP 服务器响应客户端请求。

Node.js 提供了丰富的内置模块来⽀持流的操作,包括 fs(⽂件系统)、http(HTTP 服务器与客户端)、 net(⽹络通信)等。通过使⽤流,可以有效地处理⼤量数据并提⾼性能,同时避免因为数据量过⼤⽽导致内存占⽤过⾼的问题。

6. ReadFile 和 createReadStream 函数有什么区别?

readFilecreateReadStream 是 Node.js 中⽤于读取⽂件的两种不同⽅式,它们之间有以下区别:

readFile:

  • readFile 是⼀次性将整个⽂件内容读⼊内存,并在完整读取后返回⽂件内容。适⽤于对⽂件进⾏全局操作且⽂件较⼩的情况。
  • 适合处理⼩型⽂件,但对于⼤型⽂件可能会导致内存占⽤过⾼,因为需要⼀次性加载整个⽂件。
  • 使⽤readFile 时,整个⽂件的内容将被放⼊缓冲区中,然后可以直接访问其中的数据。
1
2
3
4
5
6
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});

createReadStream:

  • createReadStream 创建了⼀个可读流,它允许以块的⽅式逐步读取⽂件内容。适⽤于需要逐步处理⼤型⽂件以避免内存耗尽的情况。
  • 更适合处理⼤型⽂件,因为它避免了⼀次性读⼊整个⽂件,减少了内存占⽤。
  • 通过监听data事件来逐块读取⽂件内容,这种⽅式也更适合对数据流执⾏⼀些操作。
1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');
let data = '';

const readStream = fs.createReadStream('example.txt', 'utf8');

readStream.on('data', (chunk) => {
data += chunk;
});

readStream.on('end', () => {
console.log(data);
});

readFile适合处理⼩型⽂件或者需要⼀次性获取整个⽂件内容的情况;

createReadStream 适合处理⼤型⽂件,以避免内存占⽤过⾼,并⽀持逐块处理数据。

7. 如何处理 Node.js 中未捕获的异常?

  1. 全局异常处理

    可以使⽤ process 对象的 uncaughtException 事件来捕获未捕获的异常,但这种⽅式并不推荐,因为在处理完异常后,并不能保证程序状态的⼀致性。

1
2
3
4
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception: ', err); // 可以进⾏⽇志记录或其他必要的清理操作
process.exit(1); // 强制退出进程,因为状态可能不再可靠
});
  1. Domain 模块(已废弃)

在较早的 Node.js 版本中,可以使⽤ Domain 模块来捕获未捕获的异常。但由于 Domain 模块存在⼀些问题,所以在新代码中不建议使⽤。

  1. 使⽤ try…catch

在合适的地⽅使⽤ try…catch 捕获可能出现异常的代码块。这种⽅式适⽤于同步代码和部分异步代码。

1
2
3
4
5
try {
// 可能会抛出异常的代码
} catch (err) {
// 处理异常
}
  1. Promise 的 catch ⽅法

对于使⽤ Promise 的异步操作,可以使⽤catch ⽅法来捕获异常。

1
2
3
4
5
6
7
somePromiseFunction()
.then((result) => {
// 处理成功情况
})
.catch((err) => {
// 处理异常
});
  1. Async/Await 的 try…catch

对于使⽤ async/await 的异步操作,可以使⽤ try…catch 来捕获异常。

1
2
3
4
5
6
7
8
async function someAsyncFunction() {
try {
const result = await somePromiseFunction();
// 处理成功情况
} catch (err) {
// 处理异常
}
}

8. Node.Js 能否充分利⽤多核处理器?

Node.js 是单线程的,因此在默认情况下⽆法充分利⽤多核处理器。但可以通过创建⼦进程(cluster 模块或 child_process 模块),每个⼦进程都可以运⾏在不同的 CPU 核⼼上,从⽽实现多核利⽤。使⽤ cluster 模块可以简化多核处理器的利⽤,它允许将应⽤程序复制到多个⼦进程中,并通过负载均衡来充分利⽤多核处理器。

9. 反应堆设计模式是什么?

反应堆设计模式(Reactor Design Pattern)是⼀种⽤于处理并发请求和事件驱动的编程模式。该模式主要包括以下⼏个关键组件:

  1. 事件驱动:基于事件的系统,其核⼼是⼀个事件循环(event loop),负责监听和分发事件。

  2. I/O 多路复⽤:使⽤诸如 select、poll 或 epoll 等机制,实现同时监视多个⽂件描述符的状态变化,从⽽通过⾮阻塞⽅式处理 I/O 事件。

  3. 回调函数:当特定事件发⽣时,执⾏预定义的回调函数来处理这些事件。这使得程序可以异步地响应各种类型的事件。

  4. ⾮阻塞 I/O:以⾮阻塞的⽅式进⾏ I/O 操作,允许系统在等待数据准备好时继续执⾏其他任务,⽽不必等待整个操作完成。

在 Node.js 中,事件驱动和⾮阻塞 I/O 是通过反应堆设计模式来实现的。这种模式使得 Node.js 能够⾼效地处理⼤量并发请求,并且在等待 I/O 操作完成时释放资源,不会阻塞其他请求的处理。

10. 单线程与多线程⽹络后端相⽐有哪些好处?

单线程⽹络后端的好处:

  1. 较少的资源消耗:单线程⽹络后端通常⽐多线程⽹络后端消耗的系统资源更少,因为不需要额外的线程管理开销。

  2. 避免竞争条件:单线程模型避免了在多线程中可能出现的竞争条件,使得代码更加简单和可预测。

  3. 易于调试:由于单线程中没有并发问题,因此调试起来相对简单。

多线程⽹络后端的好处:

  1. 更好的利⽤多核处理器:多线程⽹络后端可以更充分地利⽤多核处理器,提⾼系统的并发处理能⼒。

  2. 避免阻塞:即使某个线程被阻塞,其他线程仍然可以继续执⾏,从⽽提⾼系统的响应速度。

单线程适合于 I/O 密集型任务,因为它能够以⾮阻塞的⽅式处理⼤量的并发请求,⽽多线程适合于 CPU 密集型任务,因为它能够更充分地利⽤多核处理器的优势。选择单线程或多线程⽹络后端取决于具体的应⽤场景和需求。

11. REPL 是什么?

REPL 是 “Read-Eval-Print Loop” 的缩写,是指⼀种交互式编程环境或解释器环境。在这种环境中,⽤户可以输⼊⼀⾏代码,系统会⽴即对其进⾏解析、求值并输出结果,然后等待⽤户下⼀⾏输⼊。

在 Node.js 中,REPL 是⼀个内置的⼯具,允许开发者在命令⾏中直接输⼊ JavaScript 代码,并且能够⽴即得到执⾏结果。这使得开发者能够快速尝试、测试和调试 JavaScript 代码,⽽⽆需编写完整的程序或脚本⽂件。通过 REPL,开发者可以实时地交互式地探索 JavaScript 语⾔的特性、API 和功能,同时也⽅便⽤于代码⽚段 的测试和验证。这在学习、原型设计以及快速问题排查上都⾮常有⽤。

12. process.nextTick 和 setImmediate 有什么区别?

process.nextTick setImmediate 是 Node.js 中⽤于调度异步操作的两个重要机制,它们之间具有以下区别:

process.nextTick:

  • process.nextTick 的回调函数会在当前操作结束后⽴即执⾏,⽽且会优先于微任务队列中的 Promise 回调。
  • 这意味着 process.nextTick 的回调函数会在同⼀个事件循环中的 I/O 事件和定时器之前执 ⾏。

setImmediate:

  • setImmediate 的回调函数会在当前事件循环迭代的末尾执⾏。它会在 I/O 事件和定时器之后但在下⼀个事件循环之前执⾏。
  • 如果在当前事件循环没有其他待执⾏的操作,则 setImmediate 的回调函数会⽴即执⾏。

process.nextTick 的回调函数会在当前操作结束后⽴即执⾏,⽽ setImmediate 的回调函数 会在当前事件循环迭代的末尾执⾏,可以理解为在执⾏顺序上的微⼩差异。

13. stub 是什么?

在软件测试中,”stub” 是指⼀种测试替身或者模拟对象,⽤于代替真实的组件以便进⾏测试。⼀般情况下,stub ⽤于模拟外部依赖,例如数据库、⽹络请求、⽂件系统等,使得测试可以专注于被测代码本身,⽽不受外部环境的影响。

Stubs 主要⽤于以下⼏个⽅⾯:

  1. 隔离测试:通过使⽤ stub 替代外部依赖,能够隔离被测试的代码,确保测试的独⽴性。

  2. 模拟⾏为:可以通过 stub 模拟外部组件的⾏为,从⽽测试特定的场景和条件。

  3. 简化测试:使得测试更加可控和可预测,同时避免了测试所需的外部资源,⽐如数据库连接或者⽹络请求。

通过 stubs 可以让开发者更轻松地编写单元测试,并且确保测试环境的独⽴性,使得针对特定功能的验证变得更加容易。

14. 为什么在 express 中分离“应⽤程序”和“服务器”是⼀种好的做法?

  1. 模块化:通过将应⽤程序与服务器分离,可以使代码更加模块化和可重⽤。这样做有助于提⾼代码的清晰度和可维护性,并且使得不同部分的代码更容易被复⽤于其他项⽬或场景中。

  2. 便于测试:分离应⽤程序和服务器使得单元测试更加容易。可以针对应⽤程序和服务器各⾃进⾏测试,⽽不必同时测试两者的组合。这样的分离有助于改善代码质量和开发效率。

  3. 灵活性:将应⽤程序和服务器分离可以使得应⽤程序更容易迁移到其他⽀持 HTTP 协议的服务器上。这样做使得切换服务器框架变得更加容易,增加了开发⼈员在后期选择、更换服务器时的灵活性。

  4. 清晰的职责分离:将应⽤程序与服务器分离能够更清晰地区分它们各⾃的职责。应⽤程序专注于业务逻辑和路由处理,⽽服务器则负责处理底层的⽹络通信和请求转发,使得代码更容易理解和维护。

15. 什么是 yarn 和 npm?为什么要⽤ yarn 代替 npm 呢?

Yarn 和 npm:

  • npm(Node Package Manager):是 Node.js 的包管理⼯具,⽤于安装、更新和管理 JavaScript 包及其依赖。npm 是 Node.js 默认的包管理器,提供了⼀个庞⼤的包仓库,供开发者共享和下载代码。

  • Yarn:也是 JavaScript 的包管理⼯具,旨在解决 npm 的⼀些限制,并提供更快、更可靠的依赖管理。Yarn 由 Facebook、Google、Exponent 和 Tilde 共同开发,它通过并⾏下载、缓存和锁定依赖版本等功能提⾼了性能和安全性。

为什么要⽤ Yarn 代替 npm 呢?

  1. 性能:Yarn 在安装包时通常⽐ npm 更快,因为它可以并⾏下载依赖项,⽽ npm 默认是串⾏下载。

  2. 缓存:Yarn 会缓存每个下载过的软件包,使得再次安装时速度更快。

  3. 版本锁定:Yarn 使⽤ yarn.lock ⽂件来确保每个开发者和持续集成环境都使⽤相同的依赖版本,⽽ npm 则需要⼿动创建 package-lock.json ⽂件。

  4. 定性:Yarn 的更新频率较低,这意味着它更加稳定,且遵循语义化版本控制规则。

  5. 安全性:Yarn 有⼀个可选的安全检查⼯具yarn audit,⽤于检查已安装的软件包中是否存在安全漏洞。

16. 如何优化 Node.js 应⽤程序的性能?

  1. 使⽤适当的数据结构和算法:选择合适的数据结构和算法对于提⾼性能⾄关重要。⽐如,针对特定需求选择合适的集合类型、搜索算法或排序算法。

  2. 异步编程:利⽤ Node.js 的⾮阻塞 I/O 特性,采⽤异步编程⽅式,避免阻塞操作以提⾼并发处理能⼒。

  3. 利⽤事件驱动:充分利⽤ Node.js 的事件驱动机制,通过订阅和响应事件来进⾏⾮阻塞的处理。这样可以提⾼系统的响应速度。

  4. 合理使⽤缓存:利⽤内存缓存结果,避免重复计算。可以使⽤内置的缓存模块或者第三⽅库(如 Redis)来加速数据访问。

  5. 压缩和合并⽂件:对静态资源⽂件(如 CSS、JavaScript)进⾏压缩和合并,减少⽹络传输时间和请求次数。

  6. 数据库优化:通过索引、合理的查询语句和连接池等⼿段对数据库进⾏优化,提⾼数据库读写性能。

  7. 调优事件循环:了解事件循环的机制,避免⻓时间运⾏的同步操作,尽量将耗时的任务交给 Worker Threads 或者外部服务来处理。

  8. 监控和调优:使⽤⼯具进⾏性能监控和分析,例如 Node.js 的内置性能⼯具、第三⽅的 APM ⼯具(Application Performance Monitoring),从⽽找出性能瓶颈并进⾏相应的调优。

  9. 横向扩展:根据需求和负载情况,考虑采⽤横向扩展的⽅式增加服务器节点,实现负载均衡。

17. 什么是中间件(Middleware)?在 Express 框架中如何使⽤中间件?

中间件(Middleware) 是指在应⽤程序处理请求和发送响应之间执⾏的⼀系列函数。在 Express 框架中,中间件允许开发者可以对 HTTP 请求进⾏拦截、处理或者修改,并且在请求流转到路由处理前或者之后执⾏额外的逻辑。

使⽤中间件有助于实现以下功能:

  1. 执⾏路由特定任务:例如身份验证、⽇志记录、数据解析或安全检查。

  2. 修改请求和响应对象:可以在中间件中修改请求和响应对象,添加⾃定义的属性或⽅法,以便后续的处理能够使⽤这些信息。

  3. 错误处理:捕获并处理请求处理过程中发⽣的错误,确保应⽤程序的稳定性。

在 Express 框架中如何使⽤中间件:

  1. 内置中间件:Express 提供了若⼲内置的中间件,可以通过 app.use ⽅法来使⽤,⽐如 express.json() ⽤于解析 JSON 数据、 express.urlencoded() ⽤于解析表单数据等。

  2. 第三⽅中间件:除了内置中间件外,还可以使⽤第三⽅中间件,如 helmet ⽤于增强应⽤程序的安全性、 morgan ⽤于记录⽇志等,同样是通过 app.use ⽅法来使⽤。

  3. ⾃定义中间件:开发者可以编写⾃⼰的中间件函数,并且通过 app.use ⽅法来使⽤这些⾃定义中间件。中间件函数需要接受 reqresnext 三个参数,分别表示请求对象、响应对象和下⼀个中间件函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 内置中间件
app.use(express.json());

// 第三⽅中间件
const helmet = require('helmet');
app.use(helmet());

// ⾃定义中间件
app.use(function (req, res, next) {
// 执⾏⼀些额外的逻辑
next();
});

中间件可以单独使⽤,也可以按顺序组合使⽤,通过 app.use ⽅法将它们添加到 Express 应⽤程序的请求处理链中,从⽽影响请求的处理流程。

18. Node.js 中的模块加载机制是什么?

在 Node.js 中,模块加载机制遵循 CommonJS 模块规范。这种模块加载机制的核⼼思想是通过使⽤ require 函数引⼊模块,并且每个⽂件都被视为⼀个独⽴的模块。

Node.js 的模块加载机制如下:

  1. 模块导⼊(require):在 Node.js 中,使⽤ require 函数来导⼊其他模块。当执⾏ require 时,Node.js 会⾃动查找、加载并执⾏指定的模块。

  2. 模块导出:要使⼀个模块可以被其他模块使⽤,需要使⽤ module.exportsexports 导出模块内容。

  3. 模块路径解析:Node.js 使⽤特定的路径解析算法来确定需要加载的模块。如果模块名不是⼀个核⼼模块或者相对/绝对路径,Node.js 会根据模块路径从当前⽬录向上逐级查找 node_modules⽂件夹中的模块。

  4. 缓存:Node.js 对已经加载过的模块会进⾏缓存,以免重复加载,提⾼加载效率。

1
2
3
4
5
6
7
// 导⼊模块
const myModule = require('./myModule');

// 导出模块
module.exports = {
// 模块内容
};

Node.js 的模块加载机制遵循 CommonJS 规范,通过 requiremodule.exports 实现模块的导⼊和导出,同时利⽤路径解析和缓存机制提⾼模块加载的性能。