Node
1. Node.js 与 JavaScript 有什么不同?
Node.js 是⼀个⽤于构建可扩展⽹络应⽤的运⾏时环境,⽽ JavaScript 是⼀种⽤于在⽹⻚上进⾏交互的脚本语⾔。主要区别包括:
Node.js 是基于 V8 引擎构建的服务器端平台,允许使⽤ JavaScript 编写服务器端代码,⽽ JavaScript 通常⽤于在浏览器中运⾏。
Node.js 提供了⼀系列内置模块和 API,使得可以直接访问⽂件系统、⽹络以及操作系统功能等;⽽在浏览器端 JavaScript 不能直接进⾏这些操作。
2. 什么场景下使⽤ Node.js?优势是什么
Node.js 适合于以下场景:
⾼并发的 I/O 密集型应⽤:如实时聊天应⽤、在线游戏、消息推送等,其中⼤量并发请求需要快速响应。
数据流处理应⽤:对于需要处理⼤量数据流的应⽤程序,例如⽇志处理、实时数据监控和分析等。
单⻚⾯应⽤程序的后端服务:作为单⻚⾯应⽤程序(SPA)的后台服务,提供 API 和数据的⽀持。
Node.js 的优势:
⾮阻塞 I/O:Node.js 采⽤事件驱动、⾮阻塞 I/O 模型,能够处理⼤量并发请求⽽不会因为等待 I/O 操作⽽阻塞。
使⽤ JavaScript 全栈开发:前后端都可以使⽤ JavaScript 编程语⾔,避免了在前后端之间切换语⾔带来的不必要的复杂性。
丰富的包管理⼯具 npm:Node.js ⽣态系统庞⼤,拥有丰富的第三⽅库和模块可供使⽤,⽽ npm 是⼀个强⼤的包管理⼯具,让开发者轻松地使⽤和分享代码。
3. EventEmitter 做了什么?实现思路
EventEmitter 是 Node.js 核⼼模块之⼀,⽤于处理事件的订阅和发布。它允许多个监听器订阅某个特定事件,并在该事件发⽣时异步地调⽤这些监听器。
实现思路:
创建⼀个事件触发器:内部维护⼀个事件名到监听器数组的映射。当新的监听器被添加或事件被触发时,相应的操作会更新这个映射关系。
注册事件监听器:使⽤
on
⽅法注册事件监听器,在特定事件发⽣时执⾏回调函数。触发事件:使⽤
emit
⽅法触发相应的事件,对应的监听器会被异步地调⽤。
1 | class EventEmitter { |
4. 事件循环是什么?
JavaScript 中的事件循环:在浏览器环境下,JavaScript 使⽤事件循环来处理异步操作。当有异步任务完成时,它会被放⼊⼀个事件队列中,然后按照顺序执⾏这些任务,并将结果返回给相应的回调函数。
Node.js 中的事件循环:Node.js 也具有类似的事件循环机制,但其实现⽅式略有不同。Node.js 的事件循环基于 libuv 库实现,采⽤单线程模型,通过轮询事件队列来实现异步 I/O 操作。它允许 Node.js 执⾏⾮阻塞 I/O 操作,从⽽提⾼了应⽤程序的并发能⼒和吞吐量。
- 区别
实现机制:
- JavaScript 在浏览器中使⽤浏览器的事件循环机制,由浏览器提供底层⽀持。
- Node.js 使⽤ libuv 库实现事件循环,可以更好地控制系统资源和执⾏异步操作。
应⽤场景:
- JavaScript 的事件循环主要⽤于处理浏览器中的异步任务,例如定时器、事件监听等。
- Node.js 的事件循环适⽤于服务器端,能够处理⼤量的并发 I/O 操作,例如⽂件读写、⽹络通信等。
⾮阻塞 I/O:
- Node.js 的事件循环允许执⾏⾮阻塞 I/O 操作,使得 Node.js 能够在等待 I/O 操作的同时继续执⾏其他任务。这是 Node.js 与浏览器 JavaScript 事件循环的重要区别之⼀。
5. 流是什么?
流(Stream)在 Node.js 中是⼀种处理⽂件、⽹络数据和其他 I/O 操作的抽象概念。它允许以⼀种⾼效的⽅式按顺序读取或写⼊数据,⽽⽆需将整个数据加载到内存中。
流可以分为可读流和可写流:
可读流:⽤于从数据源(例如⽂件、⽹络连接、标准输⼊等)读取数据。它允许逐块地读取数据,通常⽤于处理⼤量数据或连续的数据流。例如,在处理⼤型⽇志⽂件时,可读流可以逐⾏读取⽂件内容⽽不必⼀次性加载整个⽂件到内存中。
可写流:⽤于向⽬标位置(例如⽂件、⽹络连接、HTTP 响应等)写⼊数据。和可读流相反,可写流允许逐块地写⼊数据,这在需要持续产⽣输出的场景下⾮常有⽤,⽐如实时⽇志记录或 HTTP 服务器响应客户端请求。
Node.js 提供了丰富的内置模块来⽀持流的操作,包括 fs(⽂件系统)、http(HTTP 服务器与客户端)、 net(⽹络通信)等。通过使⽤流,可以有效地处理⼤量数据并提⾼性能,同时避免因为数据量过⼤⽽导致内存占⽤过⾼的问题。
6. ReadFile 和 createReadStream 函数有什么区别?
readFile
和createReadStream
是 Node.js 中⽤于读取⽂件的两种不同⽅式,它们之间有以下区别:
readFile:
readFile
是⼀次性将整个⽂件内容读⼊内存,并在完整读取后返回⽂件内容。适⽤于对⽂件进⾏全局操作且⽂件较⼩的情况。- 适合处理⼩型⽂件,但对于⼤型⽂件可能会导致内存占⽤过⾼,因为需要⼀次性加载整个⽂件。
- 使⽤
readFile
时,整个⽂件的内容将被放⼊缓冲区中,然后可以直接访问其中的数据。
1 | const fs = require('fs'); |
createReadStream:
createReadStream
创建了⼀个可读流,它允许以块的⽅式逐步读取⽂件内容。适⽤于需要逐步处理⼤型⽂件以避免内存耗尽的情况。- 更适合处理⼤型⽂件,因为它避免了⼀次性读⼊整个⽂件,减少了内存占⽤。
- 通过监听
data
事件来逐块读取⽂件内容,这种⽅式也更适合对数据流执⾏⼀些操作。
1 | const fs = require('fs'); |
readFile
适合处理⼩型⽂件或者需要⼀次性获取整个⽂件内容的情况;
createReadStream
适合处理⼤型⽂件,以避免内存占⽤过⾼,并⽀持逐块处理数据。
7. 如何处理 Node.js 中未捕获的异常?
全局异常处理
可以使⽤
process
对象的uncaughtException
事件来捕获未捕获的异常,但这种⽅式并不推荐,因为在处理完异常后,并不能保证程序状态的⼀致性。
1 | process.on('uncaughtException', (err) => { |
- Domain 模块(已废弃)
在较早的 Node.js 版本中,可以使⽤ Domain 模块来捕获未捕获的异常。但由于 Domain 模块存在⼀些问题,所以在新代码中不建议使⽤。
- 使⽤ try…catch
在合适的地⽅使⽤ try…catch 捕获可能出现异常的代码块。这种⽅式适⽤于同步代码和部分异步代码。
1 | try { |
- Promise 的 catch ⽅法
对于使⽤ Promise 的异步操作,可以使⽤catch
⽅法来捕获异常。
1 | somePromiseFunction() |
- Async/Await 的 try…catch
对于使⽤ async/await 的异步操作,可以使⽤ try…catch 来捕获异常。
1 | async function someAsyncFunction() { |
8. Node.Js 能否充分利⽤多核处理器?
Node.js 是单线程的,因此在默认情况下⽆法充分利⽤多核处理器。但可以通过创建⼦进程(cluster 模块或 child_process 模块),每个⼦进程都可以运⾏在不同的 CPU 核⼼上,从⽽实现多核利⽤。使⽤ cluster 模块可以简化多核处理器的利⽤,它允许将应⽤程序复制到多个⼦进程中,并通过负载均衡来充分利⽤多核处理器。
9. 反应堆设计模式是什么?
反应堆设计模式(Reactor Design Pattern)是⼀种⽤于处理并发请求和事件驱动的编程模式。该模式主要包括以下⼏个关键组件:
事件驱动:基于事件的系统,其核⼼是⼀个事件循环(event loop),负责监听和分发事件。
I/O 多路复⽤:使⽤诸如 select、poll 或 epoll 等机制,实现同时监视多个⽂件描述符的状态变化,从⽽通过⾮阻塞⽅式处理 I/O 事件。
回调函数:当特定事件发⽣时,执⾏预定义的回调函数来处理这些事件。这使得程序可以异步地响应各种类型的事件。
⾮阻塞 I/O:以⾮阻塞的⽅式进⾏ I/O 操作,允许系统在等待数据准备好时继续执⾏其他任务,⽽不必等待整个操作完成。
在 Node.js 中,事件驱动和⾮阻塞 I/O 是通过反应堆设计模式来实现的。这种模式使得 Node.js 能够⾼效地处理⼤量并发请求,并且在等待 I/O 操作完成时释放资源,不会阻塞其他请求的处理。
10. 单线程与多线程⽹络后端相⽐有哪些好处?
单线程⽹络后端的好处:
较少的资源消耗:单线程⽹络后端通常⽐多线程⽹络后端消耗的系统资源更少,因为不需要额外的线程管理开销。
避免竞争条件:单线程模型避免了在多线程中可能出现的竞争条件,使得代码更加简单和可预测。
易于调试:由于单线程中没有并发问题,因此调试起来相对简单。
多线程⽹络后端的好处:
更好的利⽤多核处理器:多线程⽹络后端可以更充分地利⽤多核处理器,提⾼系统的并发处理能⼒。
避免阻塞:即使某个线程被阻塞,其他线程仍然可以继续执⾏,从⽽提⾼系统的响应速度。
单线程适合于 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 主要⽤于以下⼏个⽅⾯:
隔离测试:通过使⽤ stub 替代外部依赖,能够隔离被测试的代码,确保测试的独⽴性。
模拟⾏为:可以通过 stub 模拟外部组件的⾏为,从⽽测试特定的场景和条件。
简化测试:使得测试更加可控和可预测,同时避免了测试所需的外部资源,⽐如数据库连接或者⽹络请求。
通过 stubs 可以让开发者更轻松地编写单元测试,并且确保测试环境的独⽴性,使得针对特定功能的验证变得更加容易。
14. 为什么在 express 中分离“应⽤程序”和“服务器”是⼀种好的做法?
模块化:通过将应⽤程序与服务器分离,可以使代码更加模块化和可重⽤。这样做有助于提⾼代码的清晰度和可维护性,并且使得不同部分的代码更容易被复⽤于其他项⽬或场景中。
便于测试:分离应⽤程序和服务器使得单元测试更加容易。可以针对应⽤程序和服务器各⾃进⾏测试,⽽不必同时测试两者的组合。这样的分离有助于改善代码质量和开发效率。
灵活性:将应⽤程序和服务器分离可以使得应⽤程序更容易迁移到其他⽀持 HTTP 协议的服务器上。这样做使得切换服务器框架变得更加容易,增加了开发⼈员在后期选择、更换服务器时的灵活性。
清晰的职责分离:将应⽤程序与服务器分离能够更清晰地区分它们各⾃的职责。应⽤程序专注于业务逻辑和路由处理,⽽服务器则负责处理底层的⽹络通信和请求转发,使得代码更容易理解和维护。
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 呢?
性能:Yarn 在安装包时通常⽐ npm 更快,因为它可以并⾏下载依赖项,⽽ npm 默认是串⾏下载。
缓存:Yarn 会缓存每个下载过的软件包,使得再次安装时速度更快。
版本锁定:Yarn 使⽤ yarn.lock ⽂件来确保每个开发者和持续集成环境都使⽤相同的依赖版本,⽽ npm 则需要⼿动创建 package-lock.json ⽂件。
定性:Yarn 的更新频率较低,这意味着它更加稳定,且遵循语义化版本控制规则。
安全性:Yarn 有⼀个可选的安全检查⼯具
yarn audit
,⽤于检查已安装的软件包中是否存在安全漏洞。
16. 如何优化 Node.js 应⽤程序的性能?
使⽤适当的数据结构和算法:选择合适的数据结构和算法对于提⾼性能⾄关重要。⽐如,针对特定需求选择合适的集合类型、搜索算法或排序算法。
异步编程:利⽤ Node.js 的⾮阻塞 I/O 特性,采⽤异步编程⽅式,避免阻塞操作以提⾼并发处理能⼒。
利⽤事件驱动:充分利⽤ Node.js 的事件驱动机制,通过订阅和响应事件来进⾏⾮阻塞的处理。这样可以提⾼系统的响应速度。
合理使⽤缓存:利⽤内存缓存结果,避免重复计算。可以使⽤内置的缓存模块或者第三⽅库(如 Redis)来加速数据访问。
压缩和合并⽂件:对静态资源⽂件(如 CSS、JavaScript)进⾏压缩和合并,减少⽹络传输时间和请求次数。
数据库优化:通过索引、合理的查询语句和连接池等⼿段对数据库进⾏优化,提⾼数据库读写性能。
调优事件循环:了解事件循环的机制,避免⻓时间运⾏的同步操作,尽量将耗时的任务交给 Worker Threads 或者外部服务来处理。
监控和调优:使⽤⼯具进⾏性能监控和分析,例如 Node.js 的内置性能⼯具、第三⽅的 APM ⼯具(Application Performance Monitoring),从⽽找出性能瓶颈并进⾏相应的调优。
横向扩展:根据需求和负载情况,考虑采⽤横向扩展的⽅式增加服务器节点,实现负载均衡。
17. 什么是中间件(Middleware)?在 Express 框架中如何使⽤中间件?
中间件(Middleware) 是指在应⽤程序处理请求和发送响应之间执⾏的⼀系列函数。在 Express 框架中,中间件允许开发者可以对 HTTP 请求进⾏拦截、处理或者修改,并且在请求流转到路由处理前或者之后执⾏额外的逻辑。
使⽤中间件有助于实现以下功能:
执⾏路由特定任务:例如身份验证、⽇志记录、数据解析或安全检查。
修改请求和响应对象:可以在中间件中修改请求和响应对象,添加⾃定义的属性或⽅法,以便后续的处理能够使⽤这些信息。
错误处理:捕获并处理请求处理过程中发⽣的错误,确保应⽤程序的稳定性。
在 Express 框架中如何使⽤中间件:
内置中间件:Express 提供了若⼲内置的中间件,可以通过
app.use
⽅法来使⽤,⽐如express.json()
⽤于解析 JSON 数据、express.urlencoded()
⽤于解析表单数据等。第三⽅中间件:除了内置中间件外,还可以使⽤第三⽅中间件,如
helmet
⽤于增强应⽤程序的安全性、morgan
⽤于记录⽇志等,同样是通过app.use
⽅法来使⽤。⾃定义中间件:开发者可以编写⾃⼰的中间件函数,并且通过
app.use
⽅法来使⽤这些⾃定义中间件。中间件函数需要接受req
、res
和next
三个参数,分别表示请求对象、响应对象和下⼀个中间件函数。
1 | // 内置中间件 |
中间件可以单独使⽤,也可以按顺序组合使⽤,通过 app.use
⽅法将它们添加到 Express
应⽤程序的请求处理链中,从⽽影响请求的处理流程。
18. Node.js 中的模块加载机制是什么?
在 Node.js 中,模块加载机制遵循 CommonJS 模块规范。这种模块加载机制的核⼼思想是通过使⽤ require
函数引⼊模块,并且每个⽂件都被视为⼀个独⽴的模块。
Node.js 的模块加载机制如下:
模块导⼊(require):在 Node.js 中,使⽤
require
函数来导⼊其他模块。当执⾏ require 时,Node.js 会⾃动查找、加载并执⾏指定的模块。模块导出:要使⼀个模块可以被其他模块使⽤,需要使⽤
module.exports
或exports
导出模块内容。模块路径解析:Node.js 使⽤特定的路径解析算法来确定需要加载的模块。如果模块名不是⼀个核⼼模块或者相对/绝对路径,Node.js 会根据模块路径从当前⽬录向上逐级查找
node_modules
⽂件夹中的模块。缓存:Node.js 对已经加载过的模块会进⾏缓存,以免重复加载,提⾼加载效率。
1 | // 导⼊模块 |
Node.js 的模块加载机制遵循 CommonJS 规范,通过 require
和 module.exports
实现模块的导⼊和导出,同时利⽤路径解析和缓存机制提⾼模块加载的性能。