# 模塊機制
node采用模塊化結構,按照CommonJS規范定義和使用模塊,模塊與文件是一一對應關系,即加載一個模塊,實際上就是加載對應的一個模塊文件
node 的基礎中毫無疑問的應該是有關于模塊機制的方面的,也即require這個內置功能的一些原理,關于模塊互相引用的推薦先好好讀讀官方文檔,在
這里就不給大家詳細解釋了
# 熱更新
從面試官的角度看, 熱更新 是很多程序常見的問題,對客戶端而言,熱更新意味著不用換包,當然也包含著md5校驗/差異更新等。復雜問題:
對服務端而言,熱更新意味著服務不用重啟,這樣可用性較高。
在node中做熱更新代碼,牽扯到的知識點可能主要是require會有一個cache,有這個cache在,即使你更新了 .js文件,在
代碼中再次require還是會拿到之前的編譯好緩存在v8內存(code space)中的舊代碼,但是如果只是淡出的清除掉require中的cache,再次
require確實能拿到新的代碼,但是這時候很容易碰到各地維持舊的引用依舊跑的舊的代碼的問題。
不過熱更新json之類的配置文件的話,還是可以簡單的實現,更新require的cache就行,不會
有持有舊引用的問題,但是舊的引用一直被持有很容易出現內從泄露,而要熱更新配置的話,為什么不存數據庫?或者使用zookeeper之類
的服務?通過更新文件還要在發布一次,但是存數據庫直接寫個接口配個頁面多爽
# 上下文
對于node.js而言,正常情況下只有一個上下文,甚至于內置的很多方面例如 require的實現只是在啟動的時候運行了內置的函數
每個單獨的.js文件并不意味著單獨的上下文,在某個.js文件中污染了全局的作用域一樣能影響到其它的地方
而目前的node.js將VM的接口暴露了出來,可以讓你自己創建一個新的js上下文,這一點上跟前端還是區別挺大的,在執行外部代碼的
時候,通過創建新的上下文沙盒(sandbox)可以避免上下文被污染
'use strict';
const vm = require('vm');
let code =
`(function(require) {
const http = require('http');
http.createServer( (request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\\n');
}).listen(8124);
console.log('Server running at http://127.0.0.1:8124/');
})`;
vm.runInThisContext(code)(require);
問題:既然可以通過新的上下文來避免污染,那么為什么 node.js不給每一個.js文件以獨立的上下文來避免作用域被污染
問題其實有點下套,其實Node有給每個js文件獨立的上下文,但是這避免不了全局的作用域污染,實際上這是為了功能的妥協。
Node.js 模塊正常情況對作用域不會造成污染,意外創建全局變量是一種例外,可以采用嚴格模式來避免
# 包管理
------------------------------------------------------
1. a.js和b.js兩個文件互相require是否會死循環?雙方是否能導出變量?如何從設計上避免這種問題?
答:不會,先執行的導出空對象,通過導出工廠函數讓對方從函數去拿比較好避免。
模塊在導出的只是 var module = { exports: {}};中的exports,以從a.js啟動為例,a.js還沒執行完exports就是{} 在b.js的開頭拿到的就是 {} 而已。
另外還有非常基礎和常見的問題,比如:module.exports和exports的區別這里也能一并解決了,exports只是module.exports的一個引用
2. 如果a.js require了b.js,那么在b中定義的全局變量t=11能否在a中直接打印出來?
答:每個.js能獨立一個環境只是因為node幫你在外層包了一圈自執行,所以你使用 t=11 定義全局變量在其它地方當然能拿到,情況如下:
//b.js
(function (exports,require,module,__filename,__dirname){
t=11;
})();
//a.js
(function (exports,require,nodule,__filename,__dirname){
console.log(t);//11
})();
3. 如何在不重啟node進程的情況下熱更新一個js/json文件?這個問題本身是否有問題?
答:可以清除掉require的緩存? 重新require(),視具體情況還可以用VM模塊重新執行。當然這個問題可能是典型的X-Y Problem,使用js實現熱更新很容易碰到v8優化之后各地拿到緩存的引用導致熱更新js沒意義,當然熱更新json還是可以簡單一點比如用讀取文件的方式來熱更新,但是這樣也不如從redis之類的數據庫中讀取比較合理
4. 比較 AMD, CMD, CommonJS 三者的區別?
AMD,CMD,CommonJS是目前最常用的三種模塊化書寫規范。
commonjs是用在服務器端的,同步的,如nodejs
amd, cmd是用在瀏覽器端的,異步的,如requirejs和seajs
其中,amd先提出,cmd是根據commonjs和amd基礎上提出的。
根據CommonJS規范,一個單獨的文件就是一個模塊。加載模塊使用require方法,該方法讀取一個文件并執行,最后返回文件內部的exports對象。 CommonJS 加載模塊是同步的,所以只有加載完成才能執行后面的操作。像Node.js主要用于服務器的編程,加載的模塊文件一般都已經存在本地硬盤,所以加載起來比較快,不用考慮異步加載的方式,所以CommonJS規范比較適用。但如果是瀏覽器環境,要從服務器加載模塊,這是就必須采用異步模式。所以就有了 AMD? CMD 解決方案。
AMD 是 RequireJS 在推廣過程中對模塊定義的規范化產出
AMD異步加載模塊。它的模塊支持對象 函數 構造器 字符串 JSON等各種類型的模塊。
適用AMD規范適用define方法定義模塊。
CMD是SeaJS 在推廣過程中對模塊定義的規范化產出
CMD和AMD的區別有以下幾點:
1.對于依賴的模塊AMD是提前執行,CMD是延遲執行。不過RequireJS從2.0開始,也改成可以延遲執行(根據寫法不同,處理方式不通過)。
2.CMD推崇依賴就近,AMD推崇依賴前置。
3.AMD的api默認是一個當多個用,CMD嚴格的區分推崇職責單一。例如:AMD里require分全局的和局部的。CMD里面沒有全局的 require,提供 seajs.use()來實現模塊系統的加載啟動。CMD里每個API都簡單純粹。
5. 關于 node 中 require 的實現原理等
答:require原生入口代碼里面調用了__load方法用于加載文件,繼續看__load方法原生代碼里面調用了_resolveFilename方法,顧名思義,這應該是一個解析需要require的文件名的方法,繼續看_resolveFilename方法中又調用了_findPath方法。
可以看到,這里完整的顯示了node是如何根據require傳入的名稱來定位具體的文件的,他們的順序依次是:
1、先從緩存中讀取,如果沒有則繼續往下
2、判斷需要模塊路徑是否以/結尾,如果不是,則要判斷
a. 檢查是否是一個文件,如果是,則轉換為真實路徑
b. 否則如果是一個目錄,則調用tryPackage方法讀取該目錄下的package.json文件,把里面的main屬性設置為filename
c. 如果沒有讀到路徑上的文件,則通過tryExtensions嘗試在該路徑后依次加上.js,.json和.node后綴,判斷是否存在,若存在則返回加上后綴后的路徑
3、如果依然不存在,則同樣調用tryPackage方法讀取該目錄下的package.json文件,把里面的main屬性設置為filename
4、如果依然不存在,則嘗試在該路徑后依次加上index.js,index.json和index.node,判斷是否存在,若存在則返回拼接后的路徑。
5、若解析成功,則把解析得到的文件名cache起來,下次require就不用再次解析了,否則若解析失敗,則返回false
------------------------
# Promise
Promise是異步編程的一種解決方案,比傳統的解決方案--回調函數和事件--更合理和更前大
所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果,從語法上說,Promise是一個對象,從他可以獲取異步操作的消息,Promise提供統一的API,各種異步操作都可以用同樣的方法進行處理
有了Promise對象,就可以將異步操作以同步操作的方式表達出來,避免層層嵌套的回調函數
Promise對象的兩個特點:
1.對象的狀態不受外界影響
Promise對象代表一個異步操作,有三種狀態:pending(進行中),fulfilled(已成功),rejected(已失敗),只有異步操作的結果,可以決定當前是哪一種狀態,任何其它操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其它手段無法改變
2.一旦狀態改變,就不會再變,任何時候都可以得到這個結果
resolved(已定型)
promise對象的狀態改變只有兩種可能:從pending變為fulfilled和從pending變為rejected,只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時稱為已定型
缺點:
首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。
其次,如果不設置回調函數,promise內部拋出的錯誤,不會反應到外部
當處于pending狀態時,無法得知目前進展到哪一個階段
# Events(事件)
# Timers(定時器)
# 阻塞/異步
# 并行/并發
1. Promise 中 .then的第二個參數與.catch 有什么區別?
沒什么多大的區別,全都是用來處理錯誤函數的
詳情可參考:http://es6.ruanyifeng.com/#docs/promise
2. Eventemitter的emit是同步還是異步
Node.js 中 Eventemitter 的 emit 是同步的
按監聽器的注冊順序,同步地調用每個注冊到名為 eventName 事件的監聽器,并傳入提供的參數。
如果事件有監聽器,則返回 true ,否則返回 false。
3. 如何判斷接口是否異步?是否只要有回調函數就是異步?
異步:發出指令,然后去做別的事情了,所有操作完成后再執行回調
異步I/O:異步的發出I/O請求
看文檔
console.log 打印看看
看是否有 IO 操作
單純使用回調函數并不會異步, IO 操作才可能會異步, 除此之外還有使用 setTimeout 等方式實現異步.
4. nextTick,setTimeout以及setImmediate三者的區別?
5. 如何實現一個sleep函數?
function sleep(ms) {
var start = Date.now(), expire = start + ms;
while (Date.now() < expire) ;
return;
}
6. 如何實現一個異步的reduce?(在Es5中出現的數組方法)
需要了解 reduce 的情況, 是第 n 個與 n+1 的結果異步處理完之后, 在用新的結果與第 n+2 個元素繼續依次異步下去
# 進程
## Process(進程)
## Child Processes(子進程)
## Cluster(集群)
## 進程間通信
## 守護進程
1. 進程的當前工作目錄是什么?有什么作用?
2. child_process.fork與POSIX的fork有什么區別?
cluster是常見的node利用多核的方法,它是基于child_process.fork()實現的,所以cluster產生的進程之間是通過IPC來通信的,并且它也沒有拷貝父進程的空間,而是通過加入cluster.isMaster這個標識,來區分父進程以及子進程,達到類似POSIX的fork效果
3. 父進程或子進程的死亡是否回影響對方?什么是孤兒進程?
子進程的死亡不會影響父進程,不過子進程死亡時(線程組的最后一個線程,通常是‘領頭線程死亡時’)會向他的父進程發送死亡信號。反之父進程死亡,一般情況下子進程也會隨之死亡,但如果此時子進程處于可運狀態,僵死狀態等等的話,子進程將被進程1(init進程)收養,從而成為孤兒進程。
子進程死亡的時候(處于‘終止狀態’),父進程沒有及時調用wait()或waitpid()來返回死亡進程的相關信息,此時子進程還有一個PCB殘留在進程表中,被稱為僵尸進程。
4. cluster是如何保證負載均衡的?
5. 什么是守護進程?如何實現守護進程?
守護進程,是服務端方面一個很基礎的概念。很多人只知道通過pm2之類的工具可以將進程以守護進程的方式啟動,卻不了解什么是守護進程,為什么要使用守護進程
普通的進程,在用戶退出終端之后就會直接關閉,通過&啟動到后臺的進程,之后會由于會話(session組)被回收而終止進程,守護進程是不依賴終端(tty)的進程,不會因為用戶退出終端而停止運行的進程
# IO
# Buffer
# String Decoder(字符串解碼)
# Stream (流)
# Console (控制臺)
# File System (文件系統)
# Readline
# REPL
1.Buffer一般處理什么數據?其長度能否動態變化?
Buffer是node.js中用于處理二進制數據的類,其中與IO相關的操作(網絡/文件等)均基于Buffer,Buffer類的實例非常類似整數數組
但其大小是固定不變的,并且其內存在V8堆棧外分配原始內存空間。Buffer類的實例創建之后,其所占用的內存大小不能再進行調整
2.Stream的highWaterMark與drain事件是什么?二者之間的關系是?
3.Stream的pipe的作用是?在pipe的過程中數據是引用傳遞還是拷貝傳遞?
將一個可寫流附到可讀流上,同時將可寫切換到流模式,并把所有數據推給可寫流,在pipe傳遞數據的過程中,objectMode是傳遞引用,非objectMode則是拷貝一份數據傳遞下去
4.什么是文件描述符?輸入流,輸出流,錯誤流是什么?
5.console.log是同步還是異步?如何實現一個console.log?
console.log正常情況下是異步的,除非你使用new Console(stdout[,stderr])指定了一個文件的目的地
let print = (str) =>? process.stdout.write(str + '\n')
print('hello world')
6.如何同步的獲取用戶的輸入?
node中,獲取用戶的輸入其實就是讀取node進程中的輸入流的數據
而要同步讀取,則是不用異步的read接口,而是用同步的readSync接口去讀取stdin的數據可實現
7.Readline是如何實現的?
readline模塊提供了一個用于從Readble的stream中一次讀取一行的接口,當然你也可以用于讀取文件或net,http,stream.
實現上,readline在讀取TTY的數據時,是通過input.on('keypress','onkeypress')時發現用戶按下了回車鍵來判斷是新的line的,而讀取一般的stream時,則是通過緩存數據然后用正則,test來判斷是否為new line
8.REPL
Read--Eval--Print--Loop(交互式解釋器)
repl模塊提供了一種‘讀取--求值-輸出-循環的實現’它可以作為一個獨立的程序或嵌入到其它應用中
9.Cluster模塊
node默認單線程進行,對于32位系統最高可以使用512MB內存,對于64位最高可以使用1GB內存。對于多核CPU的計算機來說,這樣做效率很低,因為只有一個核在運行,其它核都在閑置。cluster模塊就是為解決這個問題而提出的
Cluster模塊允許建立一個主進程和若干個worker進程,由主進程監控和協調worker進程的運行,worker之間采用進程間通信交換信息,cluster模塊內置一個負載均衡器,采用Round-robin算法協調各個worker進程之間的負載,運行時,所有新建立的鏈接都有主進程完成,然后主進程再把TCP連接分配給指定的worker進程
10. Events模塊
回調函數模式讓node可以處理異步操作,但是,為了適應回調函數,異步操作只能有兩個狀態:開始和結束。對于那些多狀態的異步操作,回調函數就會無法處理,你不得不將異步操作拆開,分成多個階段,每個階段結束時,調用下一個回調函數。
為了解決這個問題,node提供Event Emitter接口。通過事件,解決多狀態異步操作的響應問題
11. child_process模塊
用于新建子進程,子進程的運行結果儲存在系統緩存之中(最大200KB),等到子進程運行結束后,主進程在調用回調函數讀取子進程的運行結果
#小題型
1. 什么是error-first回調模式?
錯誤優先處理回調函數,應用error-first回調模式是為了更好的進行錯誤和數據的傳遞,第一個參數保留給一個錯誤error對象,一旦出現錯誤,錯誤將通過第一個參數error返回。其余的參數將用作數據的傳遞。
2. 如何避免回調地獄?
模塊化設計:將回調函數拆分成幾個獨立的函數
使用流程控制庫,比如async
組合使用generators和Promises
使用async/await函數
3. 什么是Promises?
在 then 后沒有 catch ,沒有捕捉異常。這樣做會造成故障沉默,不會拋出異常。
如果你調試一個巨大的代碼庫,并且比不知道哪個 Promise 函數有潛在的問題, 你可以使用
unhandledRejection 這個工具。它將會打印出所有未處理的 reject 狀態的 Promise。
4. 什么工具統一團隊的代碼風格?為什么統一的代碼風格很重要?
ESLint和Standard可以用來統一代碼
因為團隊協作時,代碼風格一致,團隊成員可以更輕松的構建項目,可以通過靜態分析排除代碼問題
5. 什么時候用npm?什么時候應當用yarn?
6. 什么是樁代碼(stub)?請描述一下應用場景?
樁代碼就是在某些組件或模塊中,模擬某些功能的代碼。樁代碼的作用是占位,讓代碼在測試過程中順利進行,測試時,stub可以為函數調用返回模擬的結果,比如,當我們寫文件時,實際上并不需要真正去寫
7. 什么是測試金字塔?請舉例說明?
測試金字塔反映了單元測試,集成測試,端到端測試在測試中占的比例
8. 你最欣賞的HTTP框架是什么?為什么?
9. 如何保證你的cookie安全?如何阻止XSS攻擊?
XSS攻擊是指攻擊者向html頁面里面插入惡意JavaScript代碼
HttpOnly 這個屬性幫助防止跨站腳本攻擊,它禁止通過JavaScript訪問cookie
為了防止攻擊,可以對HTTP header里的set-cookie進行處理set-cookie:sid=;HttpOnly.
如果使用express框架,可以使用express-cookie session,它會默認做出上述防止XSS攻擊的設置
10. 如何確認項目的相關依賴安全?
自動的更新你的依賴: npm outdated
NSP
11. 什么時候應該在后臺進程中使用消息服務,怎樣處理工作想成的任務/怎么給worker安排任務?
消息隊列適用于后臺數據傳輸服務,比如發送郵件和數據圖像處理
消息隊列有很多解決方案,比如RabbitMQQ
12. 這段代碼有什么問題?
new? Promise((resolve,reject) => {
throw new Error('error')
})
.then(console.log)
then之后沒有catch。這樣的話,錯誤會被忽略,會造成故障沉默
后邊跟上 .catch(console.error)
調試一個大型項目時,可以使用監控unhandleRejection事件來捕獲所有未處理的Promise錯誤
process.on('unhandledRejection',(err) => {
console.log(err)
})
13. 這段代碼輸出什么?
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => { throw new Error('My Error') })
.catch(() => 1)
.then((x) => x + 1)
.then((x) => console.log(x))
.catch(console.error)
答案是2,逐行解釋如下:
1.創建promise,resolve的值為1
2.x 的值為1,加1 返回2
3.x 的值為2,但是沒有用到,拋出一個錯誤
4.捕獲錯誤,但是沒有處理,返回1
5.x 的值為1,加1 返回2
6.x 的值為2,打印為2
7.不會執行,因為沒有錯誤拋出
14.為什么要用node?
簡單強大,輕量可擴展
簡單體現在node使用JavaScript,json來進行編碼,人人都會
強大體現在非阻塞IO,可以適應分塊傳輸數據,較慢的網絡環境,尤其擅長高并發訪問。
輕量體現在node本身既是代碼,又是服務器,前后端使用統一語言。
可擴展體現在可以輕松應對多實例,多服務架構,同時有海量的第三方應用組件
node的優點:I/O密集型處理是node的強項,因為node的I/O請求都是異步的
node的缺點:不擅長CPU密集型的操作
CPU密集型操作(復雜的運算,圖片的操作)
15. node的架構是什么樣子的?
主要分為三層,應用app >> v8及node內置架構 >> 操作系統
v8是node運行的環境,可以理解為node虛擬機
node內置架構又可分為三層:
核心模塊(javascript實現) >> c++綁定 >> libuv + CAes + http
16. node有哪些核心模塊?
EventEmitter,Stream,FS,Net和全局對象
17. node在企業中常用到?
靜態資源服務器,代碼本地構建,單元測試,UI測試
工作中應該注意:精確版本號,測試,使用debug,保持代碼精簡
多請教,保持獨立思考,使用現有的庫,保持簡單
良好的文檔,配置文件,使用pm2
libuv:提供異步功能的C庫。它在運行是負責一個事件循環(Event loop),一個線程池,文件系統I/O,DNS相關和網絡I/O,以及其它一些功能
18. node異常處理
node是單線程運行環境,一旦拋出的異常沒有被捕獲就會引起整個進程的崩潰。
使用throw語句拋出一個錯誤對象,即拋出異常
將錯誤對象傳遞給回調函數,由回調函數負責發出錯誤
通過EventEmitter接口,發出一個error事件
try...catch結構無法捕獲異步運行的代碼拋出的錯誤
19. node關于高并發問題?
原理:非阻塞事件驅動實現異步開發,通過事件驅動的I/O來操作完成跨平臺數據密集型實時應用
傳統的server每個請求生成一個線程,node是一個單線程的,使用libuv保持數萬并發
libuv原理:
c語言編寫的基礎庫實現主循環,文件,網絡即可
libuv的改進:
回傳上下文信息
其它線程不能訪問缺省主循環,loop不支持多線程
數據庫高并發實現
var proxy = new EventProxy();
var status = "ready";
var select = function(callback){
proxy.once("selected",callback);
if(status == "ready"){
status = "pending";
db.select("SQL", function(results){
proxy.emit("selected",results);
status = "ready";
});
}
20. 什么是異步?
異步就是發出操作指令(把回調函數加入異步隊列),然后就可以去做別的事情去了,所有操作完成后再執行回調
異步I/O就是異步的發出I/O請求
雖然node可以異步發出I/O請求,但nodejs不支持多線程,為啥就可以支持高并發呢?
因為nodejs的I/O操作,底層是開啟了多線程的
當同時有多個I/O請求時,主線程會創建多個eio線程,以提高IO請求的處理速度
雖然nodeJS的IO操作開啟了多線程,但是所有線程都是基于主線程開啟的只能跑在一個進程當中還是不能充分利用CPU資源
pm2進程管理器可以解決這個問題,pm2是一個帶有負載均衡功能的node應用的進程管理器
cluster模塊
21. 代碼可讀性維護改進
async:async.waterfall([getcatalog,getaticle,getTigle])
promise的方法
koa寫法
es6寫法使用yield
22.優化問題
前端優化問題:移除iscorll,合并請求,tcp優化,HTTP優化,localstorate,html5離線緩存
api優化:restfulapi,標準輸入輸出
ui優化:使用同一的框架,前端組件化
異常處理:log監控,避免大文件處理,retry處理