前言
在樸靈老師的《深入淺出nodejs》一書中提到,每個模塊文件的require,exports和module這3個變量并沒有在模塊中定義,也并非全局函數/對象。而是在編譯的時候Node對js文件內容進行了頭尾的包裝。在頭部加了(function (exports, require, module, __filename, __dirname) {,在尾部加了 \n});。這樣看起來雖然貌似理解了require,exports和module的來源。但是依然不明白在這個函數外調用時,比如require時,發生了什么。于是花了點時間去仔細閱讀了一下node的源代碼,感覺豁然開朗,這里把我的分析過程記錄一下,僅供大家參考。
源文件分析
有關require的內容,只要閱讀兩個文件就夠了。分別是lib/internal/bootstrap_node.js和lib/module.js。
1、require入口:?
可以看到,這里繼續調用了_load方法用于加載文件
2、Module._load方法:
可以看到,這里繼續調用了_resolveFilename方法,顧名思義,這應該是一個解析需要require的文件名的方法
3、Module._resolveFilename方法:
可以看到,這里又繼續調用了_findPath方法
4、Module._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
5、文件名成功獲取后,再次回到Module._load方法:
可以看到,這里繼續調用了load方法來加載文件
6、Module.load方法:
可以看到,這里將根據不同的文件類型(js,json和node),采用不同的加載方法
7、不同文件類型的加載方法不同:
可以看到,js文件將在讀入文件(同步讀)內容后進行編譯,json文件則用JSON.parse解析內容,node文件則使用dlopen進行動態鏈接庫載入
8、這里僅針對通常的js類型的文件的載入進行分析:
可以看到,js文件內容先被wrap(包裝)了一下,然后才使用runInThisContext來運行包裝后的代碼,而傳入的參數就是前面說的exports, require, module,還有當前文件名及所在目錄名。此外,也看到,模塊中的this其實是指向module的exports,而不是global!
9、Module.wrap:
可以看到,Module.wrap只是NativeModule.wrap的引用,這里的NativeModule則位于lib/internal/bootstrap_node.js中
10、NativeModule.wrap:
可以看到,這里就對上了文章開頭所說的編譯時node文件內容的頭尾包裝,自此,本次源碼分析結果。
總結
從上面的分析可以看出來,exports其實是module的屬性,require則是Module原型的方法。exports.xx=xx,其實跟module.exports.xx=xx其實是一樣的,不過如果直接為export賦值,則不能寫成exports=xx,而應該寫成module.exports=xx,因為exports在這里只是一個引用。
從上面也可以看到,每一次require,都會把new一個Module,并且把這個Module添加到當前模塊的children中,并且返回新建的Module對象的exports。
其實node啟動的原理跟require是一樣的,src/node.cc中的node::LoadEnvironment函數會被調用,在該函數內則會接著調用lib/internal/bootstrap_node.js中的代碼,并執行startup函數,startup函數會執行Module.runMain方法,而Module.runMain方法會執行Module._load方法,參數就是命令行的第一個參數(比如: node ./app.js),如此,跟上面require就走到一起了。
要想深入的理解一件事情的原理,還是需要仔細的閱讀和研究底層的實現代碼,好在node關于require實現原理方面的代碼還挺簡單的,想要深入理解node的同學還是很有必要仔細讀一下的。