謹以此文獻給個人摯友喬治G同窗!!!願他打敗一切惡魔和狗腿
事情的原由是這樣的,喬治G在他的電腦上作了一個小測試,但結果和預期的大不相同。javascript
那麼咱們先來看看這個小測試都寫了什麼:html
一共三個文件,代碼總計不超過15行java
parent.jsnode
class Parent {} module.exports = Parent
son.jswebpack
//加載時把模塊文件名首字母大寫了(不正確的) const Parent = require('./Parent') class Son extends Parent {} module.exports = Son
test.jsgit
//加載時把模塊名首字母大寫(不正確的) 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同窗有如下疑問:github
son.js
和test.js
裏都有錯誤的文件名(大小寫問題)引用,爲何不報錯?ss instanceof ParentIncorrect === true
?不報錯我忍了,爲何還認賊做父,說本身是那個經過不正確名字加載出來的模塊的instance?若是同窗你對上述問題已經瞭然於胸,恭喜你,文能提筆安天,武能上馬定乾坤;上炕認識娘們,下炕認識鞋!web
但若是你也不是很清楚爲何?那麼好了,我有的說,你有的看。bootstrap
其實斷症(裝逼範兒的debug)之法和中醫看病也有類似指出,望、聞、問、切四招能夠按需選取一二來尋求答案。ubuntu
代碼很少,看了一會,即使沒有個人註釋,相信仔細的同窗也都發現真正的文件名和代碼中引入時有出入的,那麼這裏確定是有問題的,問題記住,咱們繼續
這個就算了,代碼我也聞不出個什麼鬼來
來吧,軟件工程裏很重要的一環,就是溝通,不見得是和遇到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'
,因此我沒瘋。這點真使人高興。
因而有了第一個問題
會不會和操做系統有關係?來咱們再找臺ubuntu
試試,果真,到了ubuntu
上,大小寫問題就是個問題了,Error: Cannot find module './Parent'
。(經朋友提醒,windows
也是默認大小寫不敏感的,因此以前舉例說windows
會報錯,應該也是我本身早前修改過註冊表緣故)。
那麼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
,進入正題:
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
,永不放棄,繼續
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
都是什麼牛鬼蛇神了。因而怎麼可以查看到Module._cache
就是咱們的下一個探索目標。那麼咱們就得順着剛纔發現的,真正的load
繼續看下去了。
Module.prototype.load = function(filename) { debug('load %j for module %j', filename, this.id); assert(!this.loaded); this.filename = filename; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; //這裏就是關鍵,根據文件名,擴展名找到了該文件,加載的好戲上演了 Module._extensions[extension](this, filename); this.loaded = true; //ES6 module相關,不看 if (ESMLoader) { ... ... ... } };
順着這條路,咱們如今應該去找那個
Module._extensions['.js']
的實現了
lib/module.js => Module._extensions
Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(internalModule.stripBOM(content), filename); };
至此,咱們尚未發現如何從開發者角度訪問到
_cache
的蹤影,因此繼續向下走
Module.prototype._compile = function(content, filename) { content = internalModule.stripShebang(content); // create wrapper function //爲了保證每一個模塊獨立的做用域,這個有個wrapper的過程, //相信瞭解browserify、webpack工做原理的朋友懂得 var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); ... ... var dirname = path.dirname(filename); //這個步驟是關鍵,看到了require,請容許我草率的決定進去看看這個makeRequireFunction var require = internalModule.makeRequireFunction(this); var depth = internalModule.requireDepth; if (depth === 0) stat.cache = new Map(); var result; if (inspectorWrapper) { result = inspectorWrapper(compiledWrapper, this.exports, this.exports, require, this, filename, dirname); } else { result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); } if (depth === 0) stat.cache = null; return result; };
lib/internal/module.js => makeRequireFunction
function makeRequireFunction(mod) { const Module = mod.constructor; function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } } function resolve(request, options) { return Module._resolveFilename(request, mod, false, options); } require.resolve = resolve; function paths(request) { return Module._resolveLookupPaths(request, mod, true); } resolve.paths = paths; require.main = process.mainModule; // Enable support to add extra extension types. require.extensions = Module._extensions; //開心,我看到Module._cache被賦值到require上了 //接下來只要知道這個require是否是咱們在使用時的那個就行了 require.cache = Module._cache; return require; }
我在這裏能夠明確告訴你,是的,這裏的require
,就是咱們代碼裏用到的require
。線索就在上面那步Module.prototype._compile
裏,請仔細看var wrapper = Module.wrap(content);
和result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
兩行內容
注:其實若是你熟讀文檔, 上述尋找_cache
訪問方法的過程是沒必要要的,但爲了保持敘事完整,我仍是裝了個逼,請見諒
打完收工,如今咱們已經知道如何查看_cache
裏的內容了,因而我在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
仍是能夠改爲大小寫敏感的,格盤重裝系統;新建分區都行。
問題雖然不難,但探究問題的決心和思路仍是重要的。
最後祝願你們前程似錦!!