讀懂CommonJS的模組載入
叨叨一會CommonJS
Common這個英文單詞的意思,相信大家都認識,我記得有一個片語common knowledge是常識的意思,那麼CommonJS是不是也是類似於常識性的,大家都理解的意思呢?很明顯不是,這個常識一點都不常識。我最初認為commonJS是一個開源的JS庫,就是那種非常方便用的庫,裡面都是一些常用的前端方法,然而我錯得離譜,CommonJS不僅不是一個庫,還是一個看不見摸不著的東西,他只是一個規範!就像校紀校規一樣,用來規範JS程式設計,束縛住前端們。就和Promise一樣是一個規範,雖然有許多實現這些規範的開源庫,但是這個規範也是可以依靠我們的JS能力實現的。
CommonJs規範
那麼CommonJS規範了些什麼呢?要解釋這個規範,就要從JS的特性說起了。JS是一種直譯式指令碼語言,也就是一邊編譯一邊執行,所以沒有模組的概念。因此CommonJS是為了完善JS在這方面的缺失而存在的一種規範。
CommonJS定義了兩個主要概念:
require
函式,用於匯入模組module.exports
變數,用於匯出模組
然而這兩個關鍵字,瀏覽器都不支援,所以我認為這是為什麼瀏覽器不支援CommonJS的原因。如果一定腰在瀏覽器上使用CommonJs,那麼就需要一些編譯庫,比如browserify來幫助哦我們將CommonJs編譯成瀏覽器支援的語法,其實就是實現require和exports。
那麼CommonJS可以用於那些方面呢?雖然CommonJS不能再瀏覽器中直接使用,但是nodejs可以基於CommonJS規範而實現的,親兒子的感覺。在nodejs中我們就可以直接使用require和exports這兩個關鍵詞來實現模組的匯入和匯出。
Nodejs中CommomJS模組的實現
require
匯入,程式碼很簡單,let {count,addCount}=require("./utils")
就可以了。那麼在匯入的時候發生了些什麼呢??首先肯定是解析路徑,系統給我們解析出一個絕對路徑,我們寫的相對對路徑是給我們看的,絕對路徑是給系統看的,畢竟絕對路徑辣麼長,看著很費力,尤其是當我們的的專案在N個資料夾之下的時候。所以requir
require
幫我們去匹配去尋找。也就是說require
的第一步是解析路徑獲取到模組內容:
- 如果是核心模組,比如
fs
,就直接返回模組 - 如果是帶有路徑的如
/
,./
等等,則拼接出一個絕對路徑,然後先讀取快取require.cache
再讀取檔案。如果沒有加字尾,則自動加字尾然後一一識別。.js
解析為JavaScript 文字檔案.json
解析JSON物件.node
解析為二進位制外掛模組
- 首次載入後的模組會快取在
require.cache
之中,所以多次載入require
,得到的物件是同一個。 - 在執行模組程式碼的時候,會將模組包裝成如下模式,以便於作用域在模組範圍之內。
(function(exports, require, module, __filename, __dirname) {
// 模組的程式碼實際上在這裡
});
module
說完了require做了些什麼事,那麼require
觸發的module
做了些什麼呢?我們看看用法,先寫一個簡單的匯出模組,寫好了模組之後,只需要把需要匯出的引數,加入module.exports
就可以了。
let count=0
function addCount(){
count++
}
module.exports={count,addCount}
然後根據require執行程式碼時需要加上的,那麼實際上我們的程式碼長成這樣:
(function(exports, require, module, __filename, __dirname) {
let count=0
function addCount(){
count++
}
module.exports={count,addCount}
});
require
的時候究竟module
發生了什麼,我們可以在vscode打斷點:
根據這個斷點,我們可以整理出:
黃色圈出來的時require
,也就是我們呼叫的方法
紅色圈出來的時Module
的工作內容
Module._compile
Module.extesions..js
Module.load
tryMouduleLoad
Module._load
Module.runMain
藍色圈出來的是nodejs乾的事,也就是NativeModule
,用於執行module
物件的。
我們都知道在JS中,函式的呼叫時棧stack的方式,也就是先近後出,也就是說require這個函式觸發之後,圖中的執行時從下到上執行的。也就是藍色框最先執行。我把他的部分程式碼扒出來,研究研究。
NativeModule
原生程式碼關鍵程式碼,這一塊用於封裝模組的。
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
等NativeModule
觸發Module.runMain
之後,我們的模組載入開始了,我們按照從下至上的順序來解讀吧。
Module._load
,就是新建一個module
物件,然後將這個新物件放入Module
快取之中。var module = new Module(filename, parent); Module._cache[filename] = module;
tryMouduleLoad
,然後就是新建的module
物件開始解析匯入的模組內容module.load(filename);
- 新建的
module
物件繼承了Module.load,這個方法就是解析檔案的型別,然後分門別類地執行 Module.extesions..js
這就幹了兩件事,讀取檔案,然後準備編譯Module._compile
終於到了編譯的環節,那麼JS怎麼執行文字?將文字變成可執行物件,js有3種方法:- eval方法
eval("console.log('aaa')")
- new Function() 模板引擎
let str="console.log(a)" new Function("aaa",str)
node執行字串,我們用高階的
vm
let vm=require("vm") let a='console.log("a")' vm.runInThisContext(a)
這裡Module用vm的方式編譯,首先是封裝一下,然後再執行,最後返回給require,我們就可以獲得執行的結果了。
var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true });
- eval方法
因為所有的模組都是封裝之後再執行的,也就說匯入的這個模組,我們只能根據module.exports
這一個對外介面來訪問內容。
總結一下
這些程式碼看的人真的很暈,其實主要流程就是require
之後解析路徑,然後觸發Module
這一個類,然後Module
的_load
的方法就是在當前模組中建立一個新module
的快取,以保證下一次再require
的時候可以直接返回而不用再次執行。然後就是這個新module的load
方法載入並通過VM執行程式碼返回物件給require
。
正因為是這樣編譯執行之後賦值給的快取,所以如果export的值是一個引數,而不是函式,那麼如果當前引數的數值改變並不會引起export的改變,因為這個賦予export的引數是靜態的,並不會引起二次執行。
CommonJs模組和ES6模組的區別
使用場景
CommonJS因為關鍵字的侷限性,因此大多用於伺服器端。而ES6的模組載入,已經有瀏覽器支援了這個特性,因此ES6可以用於瀏覽器,如果遇到不支援ES6語法的瀏覽器,可以選擇轉譯成ES5。
語法差異
ES6也是一種JavaScript的規範,它和CommonJs模組的區別,顯而易見,首先程式碼就不一樣,ES6的匯入匯出很直觀import
和export
。
commonJS | ES6 | |
---|---|---|
支援的關鍵字 | arguments,require,module,exports,__filename,__dirname |
import,export |
匯入 | const path=require("path") |
import path from "path" |
匯出 | module.exports = APP; |
export default APP |
匯入的物件 | 隨意修改 | 不能隨意修改 |
匯入次數 | 可以隨意require ,但是除了第一次,之後都是從模組快取中取得 |
在頭部匯入 |
** 大家注意了!劃重點!nodejs是CommonJS的親兒子,所以有些ES6的特性並不支援,比如ES6對於模組的關鍵字import
和export
,如果大家在nodejs環境下執行,就等著大紅的報錯吧~**
載入差異
除了語法上的差異,他們引用的模組性質是不一樣的。雖然都是模組,但是這模組的結構差異很大。
在ES6中,如果大家想要在瀏覽器中測試,可以用以下程式碼:
//utils.js
const x = 1;
export default x
<script type="module">
import x from './utils.js';
console.log(x);
export default x
</script>
首先要給script
一個type="module"
表明這裡面是ES6的模組,而且這個標籤預設是非同步載入,也就是頁面全部載入完成之後再執行,沒有這個標籤的話程式碼不然無法執行哦。然後就可以直接寫import和export了。
ES6模組匯入的幾個問題:
- 相同的模組只能引入一次,比如
x
已經匯入了,就不能再從utils中匯入x
- 不同的模組引入相同的模組,這個模組只會在首次
import
中執行。 - 引入的模組就是一個值的引用,並且是動態的,改變之後其他的相關值也會變化
- 引入的物件不可隨意斬斷連結,比如我引入的
count
我就不能修改他的值,因為這個是匯入進來的,想要修改只能在count
所在的模組修改。但是如果count
是一個物件,那麼可以改變物件的屬性,比如count.one=1
,但是不可以count={one:1}
。
大家可以看這個例子,我寫了一個改變object值的小測試,大家會發現utils.js
中的count
初始值應該是0
,但是運行了addCount
所以count
的值動態變化了,因此count
的值變成了2
。
let count=0
function addCount(){
count=count+2
}
export {count,addCount}
<script type="module">
import {count,addCount} from './utils.js';
//count=4//不可修改,會報錯
addCount()
console.log(count);
</script>
與之對比的是commonJS的模組引用,他的特性是:
- 上一節已經解釋了,模組匯出的固定值就是固定值,不會因為後期的修改而改變,除非不匯出靜態值,而改成函式,每次呼叫都去動態呼叫,那麼每次值都是最新的了。
- 匯入的物件可以隨意修改,相當於只是匯入模組中的一個副本。
CommonJS模組總結
CommonJS模組只能執行再支援此規範的環境之中,nodejs是基於CommonJS規範開發的,因此可以很完美地執行CommonJS模組,然後nodejs不支援ES6的模組規範,所以nodejs的伺服器開發大家一般使用CommonJS規範來寫。
CommonJS模組匯入用require
,匯出用module.exports
。匯出的物件需注意,如果是靜態值,而且非常量,後期可能會有所改動的,請使用函式動態獲取,否則無法獲取修改值。匯入的引數,是可以隨意改動的,所以大家使用時要小心。