挑战一轮大厂后的面试总结(含六个方向)-nodejs篇

挑战一轮大厂后的面试总结(含六个方向)-nodejs篇

2023年7月7日发(作者:)

挑战⼀轮⼤⼚后的⾯试总结(含六个⽅向)-nodejs篇在去年底开始换⼯作,直到现在算是告了⼀个段落,断断续续的也⾯试了不少公司,现在回想起来,那段时间经历了被⾯试官⼿撕,被笔试题狂怼,悲伤的时候差点留下没技术的泪⽔。这篇⽂章我打算把我找⼯作遇到的各种⾯试题(每次⾯试完我都会总结)和我⾃⼰复习遇到⽐较有意思的题⽬,做⼀份汇总,年后是跳槽⾼峰期,也许能帮到⼀些⼩伙伴。先说下这些题⽬难度,⼤部分都是基础题,因为这段经历给我的感觉就是,不管你⾯试的是⾼级还是初级,基础的知识⼀定会问到,甚⾄会有⼀定的深度,所以基础还是⾮常重要的。我将根据类型分为⼏篇⽂章来写:(已完成) 强烈⼤家看看这篇,⾯试中 js 是⼤头(已完成)(已完成)(已完成)(已完成)(已完成)六篇⽂章都已经更新完啦~这篇⽂章是对

nodejs 相关的题⽬做总结,欢迎朋友们先收藏在看。先看看⽬录Q: 怎么看 nodejs 可⽀持⾼并发这个问题涉及了好⼏个⽅⾯啊,聊的好,是个很好的加分项。可按照以下步骤给⾯试官解释1. nodejs 的单线程架构模型nodejs 其实并不是真正的单线程架构,因为 nodejs 还有I/O线程存在(⽹络I/O、磁盘I/O),这些I/O线程是由更底层的

libuv 处理,这部分线程对于开发者来说是透明的。 JavaScript 代码永远运⾏在V8上,是单线程的。所以从开发者的⾓度上来看 nodejs 是单线程的。来张⽹图:注意看图的右边有个 Event Loop,接下来要讲的重点单线程架构的优势和劣势:优势:单线程就⼀个线程在玩,省去了线程间切换的开销还有线程同步的问题,线程冲突的问题的也不需要担⼼劣势:劣势也很明显,现在起步都是 4 核,单线程没法充分利⽤ cpu 的资源单线程,⼀旦崩溃,应⽤就挂掉了,⼤家调试脚本也知道⼀旦执⾏过程报错了,本次调试就直接结束了因为只能利⽤⼀个 cpu ,⼀旦 cpu 被某个计算⼀直占⽤, cpu 得不到释放,后续的请求就会⼀直被挂起,直接⽆响应了当然这些劣势都已经有成熟的解决⽅案了,使⽤ PM2 管理进程,或者上 K8S 也可以2. 核⼼:事件循环机制那你个单线程怎么⽀持⾼并发呢?核⼼就要在于 js 引擎的事件循环机制(我觉得这个开场还挺不错)浏览器和 nodejs 的事件循环是稍有区别的,先给⾯试官简单说下事件循环的核⼼,执⾏栈、宏队列和微队列,具体的介绍可以看我以前写的⼀篇总结然后重点说 nodejs 事件循环的差异点,因不想把两个问题混在⼀起,所以独⽴成⼀个问题,具体讲解⼤家稍微往下翻看下⼀个问题的解答。3. 给出个结论 nodejs 是异步⾮阻塞的,所以能扛住⾼并发来个个栗⼦:⽐如有个客户端请求A进来,需要读取⽂件,读取⽂件后将内容整合,最后数据返回给客户端。但在读取⽂件的时候另⼀个请求进来了,那处理的流程是怎么样的?灵魂画⼿,我整了张图,⼤家理解就好请求A进⼊服务器,线程开始处理该请求A 请求需要读取⽂件,ok,交给⽂件 IO 处理,但是处理得⽐较慢,需要花 3 秒,这时候 A 请求就挂起(这个词可能不太恰当),等待通知,⽽等待的实现就是由事件循环机制实现的,在A请求等待的时候,cpu 是已经被释放的,这时候B请求进来了, cpu 就去处理B请求两个请求间,并不存在互相竞争的状态。那什么时候会出现请求阻塞呢?涉及到⼤量计算的时候,因为计算是在 js 引擎上执⾏的,执⾏栈⼀直卡着,别的函数就没法执⾏,举个栗⼦,构建⼀个层级⾮常深的⼤对象,反复对这个这个对象(ify(bigObj))4. 有机会的话可以给⾯试官扩展 同步、异步、阻塞、⾮阻塞 这个⼏个概念同步和异步关注的是消息通信机制。同步:在发起⼀个调⽤后,在没有得到结果前,该调⽤不返回,知道调⽤返回,才往下执⾏,也就是说调⽤者等待被调⽤⽅返回结果。异步:在发起⼀个调⽤后,调⽤就直接返回,不等待结果,继续往下执⾏,⽽执⾏的结果是由被调⽤⽅通过状态、通知等⽅式告知调⽤⽅,典型的异步编程模型⽐如 阻塞和⾮阻塞,关注的是在等待结果时,线程的状态。阻塞:在等待调⽤结果时,线程挂起了,不往下执⾏⾮阻塞:与上⾯相反,当前线程继续往下执⾏Q: 介绍下 nodejs 的事件循环这⾥假设⼤家已经对浏览器的事件循环有了解,看下图:如上图,事件循环中细分为这六个阶段,依次如下:1.

Timers: 定时器 Interval Timoout 回调事件,将依次执⾏定时器回调函数2.

Pending: ⼀些系统级回调将会在此阶段执⾏3.

Idle,prepare: 此阶段"仅供内部使⽤"4.

Poll: IO回调函数,这个阶段较为重要也复杂些,5.

Check: 执⾏ setImmediate() 的回调6.

Close: 执⾏ socket 的 close 事件回调开发需要关系的阶段与我们开发相关的三个阶段分别是

Timers Poll CheckTimers :执⾏定时器的回调,但注意,在 node 11 前,连续的⼏个定时器回调会连续的执⾏,⽽不是像浏览器那样,执⾏完⼀个宏任务⽴即执⾏微任务。Check :这个阶段执⾏ setImmediate() 的回调,这个事件只在 nodejs 中存在。Poll :上⾯两个阶段的触发,其实是在 poll 阶段触发的,poll 阶段的执⾏顺序是这样的。1. 先查看 check 阶段是否有事件,有的话执⾏2. 执⾏完 check 阶段后,检查 poll 阶段的队列是否有事件,若有则执⾏3. poll 的队列执⾏完成后,执⾏ check 阶段的事件在 nodejs 中也是有宏任务和微任务的, nodejs 中除了多了

ck ,宏任务、微任务的分类都是⼀致的。那么微任务是在什么时候执⾏呢?在上图,黄⾊的⼏个阶段的旁边挨着个⼩块

microtask,每个阶段执⾏后就⽴即执⾏微任务队列⾥的事件。下⾯有个栗⼦说明。微队列的栗⼦如下代码:const fs = require('fs');const ITERATIONS_MAX = 3;let iteration = 0;const timeout = setInterval(() => { ('START: setInterval', 'TIMERS PHASE'); if (iteration < ITERATIONS_MAX) { setTimeout(() => { ('eout', 'TIMERS PHASE'); }); r('./image', (err, files) => { if (err) throw err; ('r() callback: Directory contains: ' + + ' files', 'POLL PHASE'); }); setImmediate(() => { ('ediate', 'CHECK PHASE'); }); } else { ('Max interval count exceeded. Goodbye.', 'TIMERS PHASE'); clearInterval(timeout); } iteration++; ('END: setInterval', 'TIMERS PHASE');}, 0);//

第⼀次执⾏// START: setInterval TIMERS PHASE// END: setInterval TIMERS PHASE// ediate CHECK PHASE// eout TIMERS PHASE//

第⼆次执⾏// START: setInterval TIMERS PHASE// END: setInterval TIMERS PHASE// r() callback: Directory contains: 9 files POLL PHASE// r() callback: Directory contains: 9 files POLL PHASE// ediate CHECK PHASE// eout TIMERS PHASE//

第三次执⾏// START: setInterval TIMERS PHASE// END: setInterval TIMERS PHASE// ediate CHECK PHASE// r() callback: Directory contains: 9 files POLL PHASE// eout TIMERS ck关于 ck ,这个事件的优先级要⾼于其他微队列的事件,所以对于需要⽴即执⾏的回调事件可以通过该⽅法将事件放置到微队列的起始位置。如下代码:e().then(function () { ('promise1')})ck(() => { ('nextTick') ck(() => { ('nextTick') ck(() => { ('nextTick') ck(() => { ('nextTick') }) }) })})// nextTick=>nextTick=>nextTick=>timer1=>promise1与浏览器的事件循环执⾏结果的区别我们看如下代码分别在浏览器和 nodejs 中的执⾏结果setTimeout(() => { ('timer1') e().then(function() { ('promise1') })}, 0)setTimeout(() => { ('timer2') e().then(function() { ('promise2') })}, 0)对浏览器事件队列熟悉的朋友很快就可得出

浏览器中 timer1->promise1->timer2->promise2,在浏览器中微任务队列是在每个宏任务执⾏完成后⽴即执⾏的。那么在 nodejs 中呢?结果是这样的:

timer1->timer2->promise1->promise2 ,因为微任务队列是在每个阶段完成后⽴即执⾏,所以 Timer 阶段有两个回调事件,将事件依次执⾏后,在进⼊下⼀阶段的之前,先执⾏微队列中的事件。注意:这个结果是在

node 10 及以下的版本测试出来的,在 11 及以上的版本做了修改,执⾏的结果与浏览器的执⾏结果是⼀致的timer1->promise1->timer2->promise2参考⽂章:Q: nodejs 怎么创建进程线程,可以⽤在哪些场景如何开启多个⼦进程单线程的⼀个缺点是不能充分利⽤多核,所以官⽅推出了

cluster 模块, cluster 模块可以创建共享服务器端⼝的⼦进程const cluster = require('cluster');for (let i = 0; i < numCPUs; i++) { (); //

⽣成新的⼯作进程,可以使⽤ IPC

和⽗进程通信}本质还是通过

child_() 专门⽤于衍⽣新的 进程,衍⽣的 ⼦进程独⽴于⽗进程,但两者之间建⽴的 IPC 通信通道除外, 每个进程都有⾃⼰的内存,带有⾃⼰的 V8 实例如何在⼀个进程的前提下开启多个线程在 nodejs 10.0 及以上的版本,新增了

worker_threads 模块,可开启多个线程const { Worker, isMainThread, parentPort, workerData} = require('worker_threads');const worker = new Worker(__filename, { workerData: script});线程间如何传输数据:

parentPort

postMessage

on 发送监听消息共享内存:

SharedArrayBuffer 通过这个共享内存使⽤场景1. 常见的⼀个场景,在服务中若需要执⾏ shell 命令,那么就需要开启⼀个进程var exec = require('child_process').exec;exec('ls', function(error, stdout, stderr){ if(error) { ('error: ' + error); return; } ('stdout: ' + stdout);});2. 对于服务中涉及⼤量计算的,可以开启⼀个⼯作线程,由这个线程去执⾏,执⾏完毕再把结果通知给服务线程。Q: koa2 洋葱模型的实现和原理⽬前⽐较⽕的⼀个 nodejs 框架 koa2, 这个框架的代码并不多,也⾮常好理解,推荐⼤家看⼀看。问起 koa2 ,只要把它的核⼼-洋葱模型说清楚就⾏。这是⼀个段⾮常简单 koa serverconst Koa = require('koa');const app = new Koa();(async (ctx, next) => { = 'Hello World'; ('firsr before next') next() ('firsr after next')});(async (ctx, next) => { ('sencond before next') next() ('sencond after next') = 'use next';});(3500, () => { ('run on port 3500')});firsr before nextsencond before nextsencond after nextfirsr after next初始化中间件通过

⽅法将中间件函数 push 到数组中,步骤如下:1. 判断是不是中间件函数是不是⽣成器

generators ,⽬前 koa2 使⽤的异步⽅案是

async/await ,如果是

generators 函数,会转换成async/await2. 使⽤ middleware 数组存放中间件use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + '/koajs/koa/blob/master/docs/'); fn = convert(fn); } debug('use %s', fn._name || || '-'); (fn); return this;}执⾏中间件(洋葱模型)我们通过 use 注册中间件,中间件函数有两个参数第⼀个是上下⽂,第⼆个是 next,在中间件函数执⾏过程中,若遇到 next() ,那么就会进⼊到下⼀个中间件中执⾏,下⼀个中间执⾏完成后,在返回上⼀个中间件执⾏ next() 后⾯的⽅法,这便是中间件的执⾏逻辑。核⼼函数如下,我加上了注释// koa-compose/tion compose(middleware) { // middleware

函数数组 if (!y(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } /* content:上下⽂

next:新增⼀个中间件⽅法,位于所有中间件末尾,⽤于内部扩展 */ return function (context, next) { // last called middleware # let index = -1 //

计数器,⽤于判断中间是否执⾏到最后⼀个 return dispatch(0) //

开始执⾏第⼀个中间件⽅法 function dispatch(i) { if (i <= index) return (new Error('next() called multiple times')) index = i let fn = middleware[i] //

获取中间件函数 if (i === ) fn = next //

如果中间件已经到了最后⼀个,执⾏内部扩展的中间件 if (!fn) return e() //

执⾏完毕,返回 Promise try { //

执⾏ fn

,将下⼀个中间件函数赋值给 next

参数,在⾃定义的中间件⽅法中显⽰的调⽤ next

函数,中间件函数就可串联起来了 return e(fn(context, (null, i + 1))); } catch (err) { return (err) } } }}函数逻辑不难理解,妙在于设计,看官⽅张图,⾮常巧妙的利⽤函数式编程的思想(若是对函数式编程熟悉,可以给⾯试官来⼀波)Q: 介绍下 stream流在 nodejs ⽤的很⼴泛,但对于⼤部分开发者来说,更多的是使⽤流,⽐如说 HTTP 中的 request respond ,标准输⼊输出,⽂件读取(createReadStream), gulp 构建⼯具等等。流,可以理解成是⼀个管道,⽐如读取⼀个⽂件,常⽤的⽅法是从硬盘读取到内存中,在从内存中读取,这种⽅式对于⼩⽂件没问题,但若是⼤⽂件,效率就⾮常低,还有可能内存不⾜,采⽤流的⽅式,就好像给⼤⽂件插上⼀根吸管,持续的⼀点点读取⽂件的内容,管道的另⼀端收到数据,就可以进⾏处理,了解 Linux 的朋友应该⾮常熟悉这个概念。 中有四种基本的流类型:Writable - 可写⼊数据的流(例如 WriteStream())。Readable - 可读取数据的流(例如 ReadStream())。Duplex - 可读⼜可写的流(例如 )。Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 Deflate())。接触⽐较多的还是第⼀⼆种pipe 来消费可读流const fs = require('fs');//

直接读取⽂件('./', 'r', (err, data) => { if (err) { (err) } (data)})//

流的⽅式读取、写⼊let readStream = ReadStream('./');let writeStream = WriteStream('./')(writeStream).on('data', (chunk) => { //

可读流被可写流消费 (chunk) (chunk);}).on('finish', () => ('finish'))原⽣提供了 stream 模块,⼤家可以看官⽅⽂档, api ⾮常强⼤,若我们需要新建个特定的流,就需要⽤到这个模块。Q: nodejs⽇志切割⽤什么实现⽤

winston 和

winston-daily-rotate-file 实现⽇志管理和切割,⽇切和根据⼤⼩进⾏切割。(具体实现没有细看,感兴趣的盆友可以看看源码)Q: 位 字节的关系位:bit 代表⼆进制字节:1字节 = 8位Q: 关于字符编码ASCII:编码的规范标准Unicode:将全世界所有的字符包含在⼀个集合⾥,计算机只要⽀持这⼀个字符集,就能显⽰所有的字符,再也不会有乱码了。Unicode码是ASCII码的⼀个超集(superset)UTF-32 UTF-8 UTF-16 都是Unicode码的编码形式UTF-32:⽤固定长度的四个字节来表⽰每个码点UTF-8:⽤可变长度的字节来表⽰每个码点,如果只需要⼀个字节就能表⽰的,就⽤⼀个字节,⼀个不够,就⽤两个…所以,在UTF-8编码下,⼀个字符有可能由1-4个字节组成.UTF-16:结合了固定长度和可变长度,它只有两个字节和四个字节两种⽅式来表⽰码点Q: npm install 的执⾏过程以下是引⽤⽹友的总结,连接见⽂末npm 模块安装机制1. 发出npm install命令2. 查询 node_modules ⽬录之中是否已经存在指定模块3. 若存在,不再重新安装4. 若不存在5. npm 向 registry 查询模块压缩包的⽹址6. 下载压缩包,存放在根⽬录下的.npm⽬录⾥7. 解压压缩包到当前项⽬的 node_modules ⽬录npm 实现原理输⼊

npm install 命令并敲下回车后,会经历如下⼏个阶段(以 npm 5.5.1 为例):1. 执⾏⼯程⾃⾝ preinstall,当前 npm ⼯程如果定义了 preinstall 钩⼦此时会被执⾏。2. 确定⾸层依赖模块,⾸先需要做的是确定⼯程中的⾸层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。⼯程本⾝是整棵依赖树的根节点,每个⾸层依赖模块都是根节点下⾯的⼀棵⼦树,npm 会开启多进程从每个⾸层依赖模块开始逐步寻找更深层级的节点。3. 获取模块,获取模块是⼀个递归的过程,分为以下⼏步:获取模块信息。在下载⼀个模块之前,⾸先要确定其版本,这是因为 中往往是 semantic version(semver,语义化版本)。此时如果版本描述⽂件( 或 )中有该模块信息直接拿即可,如果没有则从仓库获取。如 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。获取模块实体。上⼀步会获取到模块的压缩包地址(resolved 字段),npm 会⽤此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。查找该模块依赖,如果有依赖则回到第1步,没有则停⽌。4. 安装模块,这⼀步将会更新⼯程中的

node_modules ,并执⾏模块中的⽣命周期函数(按照 preinstall、install、postinstall 的顺序)。5. 执⾏⼯程⾃⾝⽣命周期,当前 npm ⼯程如果定义了钩⼦此时会被执⾏(按照 install、postinstall、prepublish、prepare 的顺序)。最后⼀步是⽣成或更新版本描述⽂件,npm install 过程完成。模块扁平化(dedupe)⽹上有个段⼦,⼀个npm快递员:你的 node_modules 到了,⼀开门,哗啦⼀⼤堆的包上⼀步获取到的是⼀棵完整的依赖树,其中可能包含⼤量重复模块。⽐如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进⾏安装,因此会造成模块冗余。从 npm3 开始默认加⼊了⼀个

dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下⾯,也就是

node-modules 的第⼀层。当发现有重复模块时,则将其丢弃。这⾥需要对重复模块进⾏⼀个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应⼀段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到⼀个兼容版本,⽽不必版本号完全⼀致,这可以使更多冗余模块在 dedupe 过程中被去掉。⽐如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。⽽当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,⼆者不存在兼容版本。会将⼀个版本放在node_modules 中,另⼀个仍保留在依赖树⾥。举个例⼦,假设⼀个依赖树原本是这样:node_modules -- foo ---- lodash@version1-- bar ---- lodash@version2假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下⾯的形式:node_modules -- foo-- bar-- lodash(保留的版本为兼容版本)假设 version1 和 version2 为⾮兼容版本,则后⾯的版本保留在依赖树中:node_modules -- foo -- lodash@version1-- bar ---- lodash@version2⼩结以上是

nodejs 相关的总结,后续遇到有代表性的题⽬还会继续补充。⽂章中如有不对的地⽅,欢迎⼩伙伴们多多指正。谢谢⼤家~

发布者:admin,转转请注明出处:http://www.yc00.com/web/1688682659a162158.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信