Node.js面试题及答案
1.说说Node.js的事件循环机制,以及各阶段的主要工作?
答:Node.js的事件循环是处理异步操作的核心,避免了单线程阻塞。它分6个阶段,按顺序循环执行:
timers阶段:执行setTimeout、setInterval回调(到时间的才执行),比如setTimeout(()={},100)会在这个阶段检查是否到100ms。
pendingcallbacks阶段:处理上一轮没执行完的系统级回调(比如TCP连接错误回调)。
idle,prepare阶段:内部使用,开发者不用关注。
poll阶段:核心阶段,先执行已就绪的I/O回调(比如fs读取文件、HTTP请求回调),再检查是否有setImmediate回调或timers回调到期——如果有就进入下一阶段,没有就阻塞等待I/O事件。
check阶段:执行setImmediate回调(这个阶段专门处理它,比timers阶段的回调优先级高,只要poll阶段结束就会进入)。
closecallbacks阶段:处理关闭回调(比如socket.on(close,...))。
举个例子:setTimeout(()={},0)和setImmediate(()={}),如果在同步代码里,执行顺序不确定(因为setTimeout的0ms实际可能有延迟);但如果在I/O回调里(比如fs.readFile回调内),setImmediate一定先执行,因为I/O回调在poll阶段,之后直接进check阶段。
2.CommonJS和ESModule(ESM)有什么区别?实际项目中怎么兼容?
答:两者都是Node.js的模块系统,核心区别在语法、加载机制和作用域:
语法:CommonJS用require导入、module.exports导出(比如consta=require(./a);module.exports={a});ESM用import、export(比如importafrom./a.js;exportdefaulta)。
加载机制:CommonJS是运行时加载(执行到require才加载,且加载后会缓存),是值拷贝(导出后修改原模块值,导入方不会变);ESM是编译时加载(静态分析,支持Tree-Shaking),是引用传递(导出方修改,导入方会同步变)。
作用域:CommonJS模块内this指向module.exports;ESM内this是undefined。
兼容方案:
改文件后缀:ESM用.mjs,CommonJS用.cjs,Node.js会自动识别。
配置package.json:加type:module,此时.js文件默认是ESM;要写CommonJS就用.cjs,或去掉type字段(默认CommonJS)。
动态导入:ESM中可以用import()加载CommonJS模块(返回Promise),比如constfs=awaitimport(fs);CommonJS中用require加载ESM会报错,只能用import()(Node.js14.3+支持)。
3.怎么解决Node.js的“回调地狱”问题?实际开发中常用哪些方案?
答:回调地狱是多层异步回调嵌套导致代码难读(比如先读文件1,再读文件2,再发请求),常用3种方案解决:
Promise封装:把异步操作改成返回Promise的函数,用then链式调用,比如:
//原回调地狱
fs.readFile(a.txt,(err,data1)={
fs.readFile(b.txt,(err,data2)={
axios.post(/api,data2).then(res={})
})
})
//Promise封装后
functionreadFilePromise(path){
returnnewPromise((resolve,reject)={
fs.readFile(path,(err,data)=err?reject(err):resolve(data))
})
//链式调用
readFilePromise(a.txt)
.then(data1=readFilePromise(b.txt))
.then(data2=axios.post(/api,data2))
.catch(err=console.error(err))
async/await:在async函数里