喬治G在他的電腦上做了一個小測試,但結果和預期的大不相同。
那么我們先來看看這個小測試都寫了什么:
一共三個文件,代碼總計不超過15行
class Parent {}module.exports = Parent
//加載時把模塊文件名首字母大寫了(不正確的)
const Parent = require('./Parent')class Son extends Parent {}
module.exports = Son
//加載時把模塊名首字母大寫(不正確的)
const ParentIncorrect = require('./Parent')
//通過正確的模塊文件名加載(正確)
const Parent = require('./parent')const Son = require('./son')
const ss = new Son()
//測試結果
console.log(ss instanceof Parent) // false
console.log(ss instanceof ParentIncorrect) // true
喬治G同學有以下疑問:
son.js
和 test.js
里都有錯誤的文件名(大小寫問題)引用,為什么不報錯?
測試結果,為什么 ss instanceof ParentIncorrect === true
?不報錯我忍了,為什么還認賊作父,說自己是那個通過不正確名字加載出來的模塊的instance?
如果同學你對上述問題已經了然于胸,恭喜你,文能提筆安天,武能上馬定乾坤;上炕認識娘們,下炕認識鞋!
但如果你也不是很清楚為什么?那么好了,我有的說,你有的看。
其實斷癥(裝逼范兒的debug)之法和中醫看病也有相似指出,望、聞、問、切四招可以按需選取一二來尋求答案。
代碼不多,看了一會,即便沒有我的注釋,相信仔細的同學也都發現真正的文件名和代碼中引入時有出入的,那么這里肯定是有問題的,問題記住,我們繼續
這個就算了,代碼我也聞不出個什么鬼來
來吧,軟件工程里很重要的一環,就是溝通,不見得是和遇到bug的同事,可能是自己,可能是QA,當然也可能是PM或者你的老板。你沒問出自己想知道的問題;他沒說清楚自己要回答的;都完蛋。。
那么我想知道什么呢?下面兩件事作為debug的入口比較合理:
操作系統
運行環境 + 版本
你怎么測試的,命令行還是其他什么手段
答曰:macOS; node.js > 8.0
;命令行 node test.js
激動人心的深刻到來了,我要動手了。(為了完整的描述 debug
過程,我會假裝這下面的所有事情我事先都是不知道的)
準備電腦,完畢
準備運行環境 node.js > 9.3.0
, 完畢
復刻代碼,完畢
運行,日了狗,果然沒報錯,而且運行結果就是喬治G說的那樣。
為了證明我沒瞎,我又嘗試在 test.js
里 require
了一個壓根不存在的文件 require('./nidayede')
,運行代碼。
還好這次報錯了 Error: Cannot find module './nidayede'
,所以我沒瘋。這點真令人高興。
于是有了第一個問題
會不會和操作系統有關系?來我們再找臺 windows
試試,果然,到了 windows
上,大小寫問題就是個問題了, Error: Cannot find module './Parent'
。
那么 macOS
到底在干什么?連個大小寫都分不出來么?于是趕緊 google
(別問我為什么不baidu)
原來人家牛逼的 OS X
默認用了 case-insensitive
的文件系統( 詳細文檔 )。
but why?這么反人類的設計到底是為了什么?
更多解釋, 來,走你
所以,這就是你不報錯的理由?(對 node.js
指責道),但這就是全部真相了。
但事情沒完
依稀有聽過 node.js
里有什么緩存,是那個東西引起的么?于是抱著試試看的心情,我把 const ParentIncorrect = require('./Parent')
和 const Parent = require('./parent')
換了下位置,心想,這樣最先按照正確的名字加載,會不會就對了呢?
果然, 還是不對 。靠猜和裝逼是不能夠真正解決問題的
那比比 ParentIncorrect
和 Parent
呢?于是我寫了 console.log(ParentIncorrect === Parent)
,結果為 false
。所以他倆還真的不是同一個東西,那么說明問題可能在引入的部分嘍?
于是一個裝逼看 node.js
源碼的想法誕生了(其實不看,問題最終也能想明白)。 日了狗,懷著忐忑的心情,終于 clone
了一把 node.js
源碼(花了好久,真tm慢)
來,我們一起進入神秘的 node.js
源碼世界。既然我們的問題是有關 require
的,那就從她開始吧,不過找到 require
定義的過程需要點耐心,這里不詳述,只說查找的順序吧
src/node_main.cc => src/node.cc => lib/internal/bootstrap_node.js => lib/module.js
找到咯,就是這個 lib/module.js
,進入正題:
lib/module.js => require
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
return Module._load(path, this, /* isMain */ false);
};
好像沒什么卵用,對不對?她就調用了另一個方法 _load
,永不放棄,繼續
lib/module.js => _load
Module._load = function(request, parent, isMain) {
//debug代碼,么卵用,跳過
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}if (isMain && experimentalModules) {
//...
//...
//這段是給ES module用的,不看了啊
}//獲取模塊的完整路徑
var filename = Module._resolveFilename(request, parent, isMain);//緩存在這里啊?好激動有沒有?!?終于見到她老人家了
//原來這是這樣的,簡單的一批,毫無神秘感啊有木有
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}//加載native但非內部module的,不看
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}//構造全新Module實例了
var module = new Module(filename, parent);if (isMain) {
process.mainModule = module;
module.id = '.';
}//先把實例引用加緩存里
Module._cache[filename] = module;//嘗試加載模塊了
tryModuleLoad(module, filename);return module.exports;
};
似乎到這里差不多了,不過我們再深入看看 tryModuleLoad
lib/module.js => tryModuleLoad
function tryModuleLoad(module, filename) {
var threw = true;
try {
//加載模塊
module.load(filename);
threw = false;
} finally {
//要是加載失敗,從緩存里刪除
if (threw) {
delete Module._cache[filename];
}
}
}
接下來就是真正的 load
了,要不我們先停一停?
好了,分析問題的關鍵在于不忘初心,雖然到目前為止我們前進的比較順利,也很爽對不對?。但我們的此行的目的并不是爽,好像是有個什么疑惑哦!于是,我們再次梳理下問題:
son.js
里用首字母大寫(不正確)的模塊名引用了 parent.js
test.js
里,引用了兩次 parent.js
,一次用完全一致的模塊名;一次用首字母大寫的模塊名。結果發現 son instanceof require('./parent') === false
既然沒報錯的問題前面已經解決了,那么,現在看起來就是加載模塊這個部分可能出問題了,那么問題到底是什么?我們怎么驗證呢?
這個時候我看到了這么一句話 var cachedModule = Module._cache[filename];
,文件名是作為緩存的 key
,來吧,是時候看看 Module._cache
里存的模塊 key
都是什么牛鬼蛇神了,打出來看看吧,于是我在 test.js
里最后面加了一句 console.log(Object.keys(require.cache))
,我們看看打出了什么結果
false
true
[ '/Users/admin/codes/test/index.js',
'/Users/admin/codes/test/Parent.js',
'/Users/admin/codes/test/parent.js',
'/Users/admin/codes/test/son.js' ]
真相已經呼之欲出了, Module._cache
里真的出現了兩個 [p|P]arent
( macOS
默認不區分大小寫,所以她找到的其實是同一個文件;但 node.js
當真了,一看文件名不一樣,就當成不同模塊了),所以最后問題的關鍵就在于 son.js
里到底引用時用了哪個名字(上面我們用了首字母大寫的 require('./Parent.js')
),這才導致了 test.js
認賊作父的梗。
如果我們改改 son.js
,把引用換成 require('./parEND.js')
,再次執行下 test.js
看看結果如何呢?
false
false
[ '/Users/haozuo/codes/test/index.js',
'/Users/haozuo/codes/test/Parent.js',
'/Users/haozuo/codes/test/parent.js',
'/Users/haozuo/codes/test/son.js',
'/Users/haozuo/codes/test/parENT.js' ]
沒有認賊作父了對不對?再看 Module._cache
里,原來是 parENT.js
也被當成一個單獨的模塊了。
所以,假設你的模塊文件名有 n
個字符,理論上,在 macOS
大小寫不敏感的文件系統里,你能讓 node.js
將其弄出最大 2
的 n
次方個緩存來
是不是很慘!?還好 macOS
還是可以改成大小寫敏感的,格盤重裝系統;新建分區都行。
問題雖然不難,但探究問題的決心和思路還是重要的。
相關推薦:
教大家如何利用node.js創建子進程
PHP與Node.js
node.js 發布訂閱模式的方法
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com