挑戰一輪大廠後的面試總結 (含六個方向) - nodejs 篇

在去年末開始換工做,直到如今算是告了一個段落,斷斷續續的也面試了很多公司,如今回想起來,那段時間經歷了被面試官手撕,被筆試題狂懟,悲傷的時候差點留下沒技術的淚水。javascript

這篇文章我打算把我找工做遇到的各類面試題(每次面試完我都會總結)和我本身複習遇到比較有意思的題目,作一份彙總,年後是跳槽高峯期,也許能幫到一些小夥伴。css

先說下這些題目難度,大部分都是基礎題,由於這段經歷給個人感受就是,無論你面試的是高級仍是初級,基礎的知識必定會問到,甚至會有必定的深度,因此基礎仍是很是重要的。html

我將根據類型分爲幾篇文章來寫:vue

面試總結:javascript 面試點彙總(已完成)java

面試總結:nodejs 面試點彙總(已完成)node

面試總結:瀏覽器相關 面試點彙總(已完成)git

面試總結:css 面試點彙總(已完成)github

面試總結:框架 vue 和工程相關的面試點彙總(已完成)面試

面試總結:非技術問題彙總(已完成)shell

我會抓緊時間把未完成的總結補全的~

這篇文章是對 nodejs 相關的題目作總結,歡迎朋友們先收藏在看。

先看看目錄

目錄

Q: 怎麼看 nodejs 可支持高併發

這個問題涉及了好幾個方面啊,聊的好,是個很好的加分項。可按照如下步驟給面試官解釋

  1. nodejs 的單線程架構模型

nodejs 其實並非真正的單線程架構,由於 nodejs 還有I/O線程存在(網絡I/O、磁盤I/O),這些I/O線程是由更底層的 libuv 處理,這部分線程對於開發者來講是透明的。 JavaScript 代碼永遠運行在V8上,是單線程的。

因此從開發者的角度上來看 nodejs 是單線程的。

來張網圖:

v8

注意看圖的右邊有個 Event Loop,接下來要講的重點

單線程架構的優點和劣勢:

優點:

  • 單線程就一個線程在玩,省去了線程間切換的開銷
  • 還有線程同步的問題,線程衝突的問題的也不須要擔憂

劣勢:

  • 劣勢也很明顯,如今起步都是 4 核,單線程無法充分利用 cpu 的資源
  • 單線程,一旦崩潰,應用就掛掉了,你們調試腳本也知道一旦執行過程報錯了,本次調試就直接結束了
  • 由於只能利用一個 cpu ,一旦 cpu 被某個計算一直佔用, cpu 得不到釋放,後續的請求就會一直被掛起,直接無響應了

固然這些劣勢都已經有成熟的解決方案了,使用 PM2 管理進程,或者上 K8S 也能夠

  1. 核心:事件循環機制

那你個單線程怎麼支持高併發呢?

核心就要在於 js 引擎的事件循環機制(我以爲這個開場還挺不錯)

瀏覽器和 nodejs 的事件循環是稍有區別的,先給面試官簡單說下事件循環的核心,執行棧、宏隊列和微隊列,具體的介紹能夠看我之前寫的一篇總結 js 事件循環

而後重點說 nodejs 事件循環的差別點,因不想把兩個問題混在一塊兒,因此獨立成一個問題,具體講解你們稍微往下翻看下一個問題的解答。

  1. 給出個結論 nodejs 是異步非阻塞的,因此能扛住高併發

來個個栗子:

好比有個客戶端請求A進來,須要讀取文件,讀取文件後將內容整合,最後數據返回給客戶端。但在讀取文件的時候另外一個請求進來了,那處理的流程是怎麼樣的?

靈魂畫手,我整了張圖,你們理解就好

loop

  • 請求A進入服務器,線程開始處理該請求
  • A 請求須要讀取文件,ok,交給文件 IO 處理,可是處理得比較慢,須要花 3 秒,這時候 A 請求就掛起(這個詞可能不太恰當),等待通知,而等待的實現就是由事件循環機制實現的,
  • 在A請求等待的時候,cpu 是已經被釋放的,這時候B請求進來了, cpu 就去處理B請求
  • 兩個請求間,並不存在互相競爭的狀態。那何時會出現請求阻塞呢?涉及到大量計算的時候,由於計算是在 js 引擎上執行的,執行棧一直卡着,別的函數就無法執行,舉個栗子,構建一個層級很是深的大對象,反覆對這個這個對象 JSON.parse(JSON.stringify(bigObj))
  1. 有機會的話能夠給面試官擴展 同步、異步、阻塞、非阻塞 這個幾個概念

同步和異步關注的是消息通訊機制。

  • 同步:在發起一個調用後,在沒有獲得結果前,該調用不返回,知道調用返回,才往下執行,也就是說調用者等待被調用方返回結果。

  • 異步:在發起一個調用後,調用就直接返回,不等待結果,繼續往下執行,而執行的結果是由被調用方經過狀態、通知等方式告知調用方,典型的異步編程模型好比 Node.js

阻塞和非阻塞,關注的是在等待結果時,線程的狀態。

  • 阻塞:在等待調用結果時,線程掛起了,不往下執行
  • 非阻塞:與上面相反,當前線程繼續往下執行

參考資料: www.zhihu.com/question/19… zhuanlan.zhihu.com/p/41118827

Q: 介紹下 nodejs 的事件循環

這裏假設你們已經對瀏覽器的事件循環有了解,看下圖:

node-loop

如上圖,事件循環中細分爲這六個階段,依次以下:

  1. Timers: 定時器 Interval Timoout 回調事件,將依次執行定時器回調函數
  2. Pending: 一些系統級回調將會在此階段執行
  3. Idle,prepare: 此階段"僅供內部使用"
  4. Poll: IO回調函數,這個階段較爲重要也複雜些,
  5. Check: 執行 setImmediate() 的回調
  6. Close: 執行 socket 的 close 事件回調

開發須要關係的階段

與咱們開發相關的三個階段分別是 Timers Poll Check

Timers :執行定時器的回調,但注意,在 node 11 前,連續的幾個定時器回調會連續的執行,而不是像瀏覽器那樣,執行完一個宏任務當即執行微任務。

Check :這個階段執行 setImmediate() 的回調,這個事件只在 nodejs 中存在。

Poll :上面兩個階段的觸發,實際上是在 poll 階段觸發的,poll 階段的執行順序是這樣的。

  1. 先查看 check 階段是否有事件,有的話執行
  2. 執行完 check 階段後,檢查 poll 階段的隊列是否有事件,如有則執行
  3. poll 的隊列執行完成後,執行 check 階段的事件

在 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

關於 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

參考文章:

www.ibm.com/developerwo…

juejin.cn/post/684490…

Q: nodejs 怎麼建立進程線程,能夠用在哪些場景

如何開啓多個子進程

單線程的一個缺點是不能充分利用多核,因此官方推出了 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 經過這個共享內存

使用場景

  1. 常見的一個場景,在服務中若須要執行 shell 命令,那麼就須要開啓一個進程
var exec = require('child_process').exec;
exec('ls', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
});
複製代碼
  1. 對於服務中涉及大量計算的,能夠開啓一個工做線程,由這個線程去執行,執行完畢再把結果通知給服務線程。

參考鏈接: wolfx.cn/nodejs/node…

Q: koa2 洋蔥模型的實現和原理

目前比較火的一個 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 到數組中,步驟以下:

  1. 判斷是否是中間件函數是否是生成器 generators ,目前 koa2 使用的異步方案是 async/await ,若是是 generators 函數,會轉換成 async/await

  2. 使用 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)
            }
        }
    }
}
複製代碼

函數邏輯不難理解,妙在於設計,看官方張圖,很是巧妙的利用函數式編程的思想(如果對函數式編程熟悉,能夠給面試官來一波)

koa2

Q: 介紹下 stream

流在 nodejs 用的很普遍,但對於大部分開發者來講,更多的是使用流,好比說 HTTP 中的 request respond ,標準輸入輸出,文件讀取(createReadStream), gulp 構建工具等等。

流,能夠理解成是一個管道,好比讀取一個文件,經常使用的方法是從硬盤讀取到內存中,在從內存中讀取,這種方式對於小文件沒問題,但如果大文件,效率就很是低,還有可能內存不足,採用流的方式,就好像給大文件插上一根吸管,持續的一點點讀取文件的內容,管道的另外一端收到數據,就能夠進行處理,瞭解 Linux 的朋友應該很是熟悉這個概念。

Node.js 中有四種基本的流類型:

  • Writable - 可寫入數據的流(例如 fs.createWriteStream())。
  • Readable - 可讀取數據的流(例如 fs.createReadStream())。
  • Duplex - 可讀又可寫的流(例如 net.Socket)。
  • Transform - 在讀寫過程當中能夠修改或轉換數據的 Duplex 流(例如 zlib.createDeflate())。 接觸比較多的仍是第一二種 pipe 來消費可讀流
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…

nodejs.cn/api/stream.…

Q: nodejs日誌切割用什麼實現

winstonwinston-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. 獲取模塊,獲取模塊是一個遞歸的過程,分爲如下幾步:
  • 獲取模塊信息。在下載一個模塊以前,首先要肯定其版本,這是由於 package.json 中每每是 semantic version(semver,語義化版本)。此時若是版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有該模塊信息直接拿便可,若是沒有則從倉庫獲取。如 packaeg.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中獲取符合 1.x.x 形式的最新版本。
  • 獲取模塊實體。上一步會獲取到模塊的壓縮包地址(resolved 字段),npm 會用此地址檢查本地緩存,緩存中有就直接拿,若是沒有則從倉庫下載。
  • 查找該模塊依賴,若是有依賴則回到第1步,沒有則中止。
  1. 安裝模塊,這一步將會更新工程中的 node_modules ,並執行模塊中的生命週期函數(按照 preinstall、install、postinstall 的順序)。

  2. 執行工程自身生命週期,當前 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
複製代碼

引用文章: muyiy.cn/question/to…

小結

以上是 nodejs 相關的總結,後續遇到有表明性的題目還會繼續補充。

文章中若有不對的地方,歡迎小夥伴們多多指正。

謝謝你們~

相關文章
相關標籤/搜索