在去年末開始換工做,直到如今算是告了一個段落,斷斷續續的也面試了很多公司,如今回想起來,那段時間經歷了被面試官手撕,被筆試題狂懟,悲傷的時候差點留下沒技術的淚水。javascript
這篇文章我打算把我找工做遇到的各類面試題(每次面試完我都會總結)和我本身複習遇到比較有意思的題目,作一份彙總,年後是跳槽高峯期,也許能幫到一些小夥伴。css
先說下這些題目難度,大部分都是基礎題,由於這段經歷給個人感受就是,無論你面試的是高級仍是初級,基礎的知識必定會問到,甚至會有必定的深度,因此基礎仍是很是重要的。html
我將根據類型分爲幾篇文章來寫:vue
面試總結:javascript 面試點彙總(已完成)java
面試總結:nodejs 面試點彙總(已完成)node
面試總結:瀏覽器相關 面試點彙總(已完成)git
面試總結:css 面試點彙總(已完成)github
面試總結:框架 vue 和工程相關的面試點彙總(已完成)面試
面試總結:非技術問題彙總(已完成)shell
我會抓緊時間把未完成的總結補全的~
這篇文章是對 nodejs
相關的題目作總結,歡迎朋友們先收藏在看。
先看看目錄
這個問題涉及了好幾個方面啊,聊的好,是個很好的加分項。可按照如下步驟給面試官解釋
nodejs 其實並非真正的單線程架構,由於 nodejs 還有I/O線程存在(網絡I/O、磁盤I/O),這些I/O線程是由更底層的 libuv
處理,這部分線程對於開發者來講是透明的。 JavaScript 代碼永遠運行在V8上,是單線程的。
因此從開發者的角度上來看 nodejs 是單線程的。
來張網圖:
注意看圖的右邊有個 Event Loop,接下來要講的重點
單線程架構的優點和劣勢:
優點:
劣勢:
固然這些劣勢都已經有成熟的解決方案了,使用 PM2 管理進程,或者上 K8S 也能夠
那你個單線程怎麼支持高併發呢?
核心就要在於 js 引擎的事件循環機制(我以爲這個開場還挺不錯)
瀏覽器和 nodejs 的事件循環是稍有區別的,先給面試官簡單說下事件循環的核心,執行棧、宏隊列和微隊列,具體的介紹能夠看我之前寫的一篇總結 js 事件循環
而後重點說 nodejs 事件循環的差別點,因不想把兩個問題混在一塊兒,因此獨立成一個問題,具體講解你們稍微往下翻看下一個問題的解答。
來個個栗子:
好比有個客戶端請求A進來,須要讀取文件,讀取文件後將內容整合,最後數據返回給客戶端。但在讀取文件的時候另外一個請求進來了,那處理的流程是怎麼樣的?
靈魂畫手,我整了張圖,你們理解就好
JSON.parse(JSON.stringify(bigObj))
同步和異步關注的是消息通訊機制。
同步:在發起一個調用後,在沒有獲得結果前,該調用不返回,知道調用返回,才往下執行,也就是說調用者等待被調用方返回結果。
異步:在發起一個調用後,調用就直接返回,不等待結果,繼續往下執行,而執行的結果是由被調用方經過狀態、通知等方式告知調用方,典型的異步編程模型好比 Node.js
阻塞和非阻塞,關注的是在等待結果時,線程的狀態。
參考資料: www.zhihu.com/question/19… zhuanlan.zhihu.com/p/41118827
這裏假設你們已經對瀏覽器的事件循環有了解,看下圖:
如上圖,事件循環中細分爲這六個階段,依次以下:
Timers
: 定時器 Interval Timoout 回調事件,將依次執行定時器回調函數Pending
: 一些系統級回調將會在此階段執行Idle,prepare
: 此階段"僅供內部使用"Poll
: IO回調函數,這個階段較爲重要也複雜些,Check
: 執行 setImmediate() 的回調Close
: 執行 socket 的 close 事件回調與咱們開發相關的三個階段分別是 Timers Poll Check
Timers
:執行定時器的回調,但注意,在 node 11 前,連續的幾個定時器回調會連續的執行,而不是像瀏覽器那樣,執行完一個宏任務當即執行微任務。
Check
:這個階段執行 setImmediate() 的回調,這個事件只在 nodejs 中存在。
Poll
:上面兩個階段的觸發,實際上是在 poll 階段觸發的,poll 階段的執行順序是這樣的。
在 nodejs 中也是有宏任務和微任務的, nodejs 中除了多了 process.nextTick
,宏任務、微任務的分類都是一致的。
那麼微任務是在何時執行呢?
在上圖,黃色的幾個階段的旁邊挨着個小塊 microtask
,每一個階段執行後就當即執行微任務隊列裏的事件。
下面有個栗子說明。
以下代碼:
const fs = require('fs'); const ITERATIONS_MAX = 3; let iteration = 0; const timeout = setInterval(() => { console.log('START: setInterval', 'TIMERS PHASE'); if (iteration < ITERATIONS_MAX) { setTimeout(() => { console.log('setInterval.setTimeout', 'TIMERS PHASE'); }); fs.readdir('./image', (err, files) => { if (err) throw err; console.log('fs.readdir() callback: Directory contains: ' + files.length + ' files', 'POLL PHASE'); }); setImmediate(() => { console.log('setInterval.setImmediate', 'CHECK PHASE'); }); } else { console.log('Max interval count exceeded. Goodbye.', 'TIMERS PHASE'); clearInterval(timeout); } iteration++; console.log('END: setInterval', 'TIMERS PHASE'); }, 0); // 第一次執行 // START: setInterval TIMERS PHASE // END: setInterval TIMERS PHASE // setInterval.setImmediate CHECK PHASE // setInterval.setTimeout TIMERS PHASE // 第二次執行 // START: setInterval TIMERS PHASE // END: setInterval TIMERS PHASE // fs.readdir() callback: Directory contains: 9 files POLL PHASE // fs.readdir() callback: Directory contains: 9 files POLL PHASE // setInterval.setImmediate CHECK PHASE // setInterval.setTimeout TIMERS PHASE // 第三次執行 // START: setInterval TIMERS PHASE // END: setInterval TIMERS PHASE // setInterval.setImmediate CHECK PHASE // fs.readdir() callback: Directory contains: 9 files POLL PHASE // setInterval.setTimeout TIMERS PHASE 複製代碼
關於 process.nextTick ,這個事件的優先級要高於其餘微隊列的事件,因此對於須要當即執行的回調事件能夠經過該方法將事件放置到微隊列的起始位置。
以下代碼:
Promise.resolve().then(function () { console.log('promise1') }) process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) }) }) // nextTick=>nextTick=>nextTick=>timer1=>promise1 複製代碼
咱們看以下代碼分別在瀏覽器和 nodejs 中的執行結果
setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) 複製代碼
對瀏覽器事件隊列熟悉的朋友很快就可得出 瀏覽器中 timer1->promise1->timer2->promise2
,在瀏覽器中微任務隊列是在每一個宏任務執行完成後當即執行的。
那麼在 nodejs 中呢?
結果是這樣的: timer1->timer2->promise1->promise2
,由於微任務隊列是在每一個階段完成後當即執行,因此 Timer 階段有兩個回調事件,將事件依次執行後,在進入下一階段的以前,先執行微隊列中的事件。
注意:這個結果是在 node 10
及如下的版本測試出來的,在 11 及以上的版本作了修改,執行的結果與瀏覽器的執行結果是一致的
timer1->promise1->timer2->promise2
參考文章:
單線程的一個缺點是不能充分利用多核,因此官方推出了 cluster
模塊, cluster 模塊能夠建立共享服務器端口的子進程
const cluster = require('cluster'); for (let i = 0; i < numCPUs; i++) { cluster.fork(); // 生成新的工做進程,可使用 IPC 和父進程通訊 } 複製代碼
本質仍是經過 child_process.fork()
專門用於衍生新的 Node.js 進程,衍生的 Node.js 子進程獨立於父進程,但二者之間創建的 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
經過這個共享內存var exec = require('child_process').exec; exec('ls', function(error, stdout, stderr){ if(error) { console.error('error: ' + error); return; } console.log('stdout: ' + stdout); }); 複製代碼
參考鏈接: wolfx.cn/nodejs/node…
目前比較火的一個 nodejs 框架 koa2, 這個框架的代碼並很少,也很是好理解,推薦你們看一看。
問起 koa2 ,只要把它的核心-洋蔥模型說清楚就行。
這是一個段很是簡單 koa server
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { ctx.body = 'Hello World'; console.log('firsr before next') next() console.log('firsr after next') }); app.use(async (ctx, next) => { console.log('sencond before next') next() console.log('sencond after next') ctx.body = 'use next'; }); app.listen(3500, () => { console.log('run on port 3500') }); 複製代碼
請求 http://127.0.0.1:3500/
輸出
firsr before next
sencond before next
sencond after next
firsr after next
複製代碼
經過 app.use
方法將中間件函數 push 到數組中,步驟以下:
判斷是否是中間件函數是否是生成器 generators
,目前 koa2 使用的異步方案是 async/await
,若是是 generators
函數,會轉換成 async/await
使用 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 ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } 複製代碼
咱們經過 use 註冊中間件,中間件函數有兩個參數第一個是上下文,第二個是 next,在中間件函數執行過程當中,若遇到 next() ,那麼就會進入到下一個中間件中執行,下一個中間執行完成後,在返回上一個中間件執行 next() 後面的方法,這即是中間件的執行邏輯。
核心函數以下,我加上了註釋
// koa-compose/index.js function compose(middleware) { // middleware 函數數組 if (!Array.isArray(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 Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] // 獲取中間件函數 if (i === middleware.length) fn = next // 若是中間件已經到了最後一個,執行內部擴展的中間件 if (!fn) return Promise.resolve() // 執行完畢,返回 Promise try { // 執行 fn ,將下一個中間件函數賦值給 next 參數,在自定義的中間件方法中顯示的調用 next 函數,中間件函數就可串聯起來了 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } 複製代碼
函數邏輯不難理解,妙在於設計,看官方張圖,很是巧妙的利用函數式編程的思想(如果對函數式編程熟悉,能夠給面試官來一波)
流在 nodejs 用的很普遍,但對於大部分開發者來講,更多的是使用流,好比說 HTTP 中的 request respond ,標準輸入輸出,文件讀取(createReadStream), gulp 構建工具等等。
流,能夠理解成是一個管道,好比讀取一個文件,經常使用的方法是從硬盤讀取到內存中,在從內存中讀取,這種方式對於小文件沒問題,但如果大文件,效率就很是低,還有可能內存不足,採用流的方式,就好像給大文件插上一根吸管,持續的一點點讀取文件的內容,管道的另外一端收到數據,就能夠進行處理,瞭解 Linux 的朋友應該很是熟悉這個概念。
Node.js 中有四種基本的流類型:
const fs = require('fs'); // 直接讀取文件 fs.open('./xxx.js', 'r', (err, data) => { if (err) { console.log(err) } console.log(data) }) // 流的方式讀取、寫入 let readStream = fs.createReadStream('./a.js'); let writeStream = fs.createWriteStream('./b.js') readStream.pipe(writeStream).on('data', (chunk) => { // 可讀流被可寫流消費 console.log(chunk) writeStream.write(chunk); }).on('finish', () => console.log('finish')) 複製代碼
原生提供了 stream 模塊,你們能夠看官方文檔, api 很是強大,若咱們須要新建個特定的流,就須要用到這個模塊。
推薦文檔: javascript.ruanyifeng.com/nodejs/stre…
用 winston
和 winston-daily-rotate-file
實現日誌管理和切割,日切和根據大小進行切割。
(具體實現沒有細看,感興趣的盆友能夠看看源碼)
位:bit 表明二進制 字節:1字節 = 8位
ASCII:編碼的規範標準
Unicode:將全世界全部的字符包含在一個集合裏,計算機只要支持這一個字符集,就能顯示全部的字符,不再會有亂碼了。Unicode碼是ASCII碼的一個超集(superset)
UTF-32 UTF-8 UTF-16
都是Unicode碼的編碼形式
UTF-32:用固定長度的四個字節來表示每一個碼點
UTF-8:用可變長度的字節來表示每一個碼點,若是隻須要一個字節就能表示的,就用一個字節,一個不夠,就用兩個…因此,在UTF-8編碼下,一個字符有可能由1-4個字節組成.
UTF-16:結合了固定長度和可變長度,它只有兩個字節和四個字節兩種方式來表示碼點
如下是引用網友的總結,鏈接見文末
輸入 npm install
命令並敲下回車後,會經歷以下幾個階段(以 npm 5.5.1 爲例):
安裝模塊,這一步將會更新工程中的 node_modules
,並執行模塊中的生命週期函數(按照 preinstall、install、postinstall 的順序)。
執行工程自身生命週期,當前 npm 工程若是定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。
最後一步是生成或更新版本描述文件,npm install 過程完成。
網上有個段子,一個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
複製代碼
引用文章: muyiy.cn/question/to…
以上是 nodejs
相關的總結,後續遇到有表明性的題目還會繼續補充。
文章中若有不對的地方,歡迎小夥伴們多多指正。
謝謝你們~