讀懂CommonJS的模塊加載
叨叨一會(huì)CommonJS
Common這個(gè)英文單詞的意思,相信大家都認(rèn)識(shí),我記得有一個(gè)詞組common knowledge是常識(shí)的意思,那么CommonJS是不是也是類似于常識(shí)性的,大家都理解的意思呢?很明顯不是,這個(gè)常識(shí)一點(diǎn)都不常識(shí)。我最初認(rèn)為commonJS是一個(gè)開源的JS庫(kù),就是那種非常方便用的庫(kù),里面都是一些常用的前端方法,然而我錯(cuò)得離譜,CommonJS不僅不是一個(gè)庫(kù),還是一個(gè)看不見摸不著的東西,他只是一個(gè)規(guī)范!就像校紀(jì)校規(guī)一樣,用來規(guī)范JS編程,束縛住前端們。就和Promise一樣是一個(gè)規(guī)范,雖然有許多實(shí)現(xiàn)這些規(guī)范的開源庫(kù),但是這個(gè)規(guī)范也是可以依靠我們的JS能力實(shí)現(xiàn)的。
CommonJs規(guī)范
那么CommonJS規(guī)范了些什么呢?要解釋這個(gè)規(guī)范,就要從JS的特性說起了。JS是一種直譯式腳本語言,也就是一邊編譯一邊運(yùn)行,所以沒有模塊的概念。因此CommonJS是為了完善JS在這方面的缺失而存在的一種規(guī)范。
CommonJS定義了兩個(gè)主要概念:
- require函數(shù),用于導(dǎo)入模塊
- module.exports變量,用于導(dǎo)出模塊
然而這兩個(gè)關(guān)鍵字,瀏覽器都不支持,所以我認(rèn)為這是為什么瀏覽器不支持CommonJS的原因。如果一定腰在瀏覽器上使用CommonJs,那么就需要一些編譯庫(kù),比如browserify來幫助哦我們將CommonJs編譯成瀏覽器支持的語法,其實(shí)就是實(shí)現(xiàn)require和exports。
那么CommonJS可以用于那些方面呢?雖然CommonJS不能再瀏覽器中直接使用,但是nodejs可以基于CommonJS規(guī)范而實(shí)現(xiàn)的,親兒子的感覺。在nodejs中我們就可以直接使用require和exports這兩個(gè)關(guān)鍵詞來實(shí)現(xiàn)模塊的導(dǎo)入和導(dǎo)出。
Nodejs中CommomJS模塊的實(shí)現(xiàn)
require
導(dǎo)入,代碼很簡(jiǎn)單,let {count,addCount}=require("./utils")就可以了。那么在導(dǎo)入的時(shí)候發(fā)生了些什么呢??首先肯定是解析路徑,系統(tǒng)給我們解析出一個(gè)絕對(duì)路徑,我們寫的相對(duì)對(duì)路徑是給我們看的,絕對(duì)路徑是給系統(tǒng)看的,畢竟絕對(duì)路徑辣么長(zhǎng),看著很費(fèi)力,尤其是當(dāng)我們的的項(xiàng)目在N個(gè)文件夾之下的時(shí)候。所以require第一件事就是解析路徑。我們可以寫的很簡(jiǎn)潔,只需要寫出相對(duì)路徑和文件名即可,連后綴都可以省略,讓require幫我們?nèi)テヅ淙ふ摇R簿褪钦frequire的第一步是解析路徑獲取到模塊內(nèi)容:
如果是核心模塊,比如fs,就直接返回模塊
如果是帶有路徑的如/,./等等,則拼接出一個(gè)絕對(duì)路徑,然后先讀取緩存require.cache再讀取文件。如果沒有加后綴,則自動(dòng)加后綴然后一一識(shí)別。
- .js 解析為JavaScript 文本文件
- .json解析JSON對(duì)象
- .node解析為二進(jìn)制插件模塊
首次加載后的模塊會(huì)緩存在require.cache之中,所以多次加載require,得到的對(duì)象是同一個(gè)。
在執(zhí)行模塊代碼的時(shí)候,會(huì)將模塊包裝成如下模式,以便于作用域在模塊范圍之內(nèi)。
(function(exports, require, module, __filename, __dirname) {
// 模塊的代碼實(shí)際上在這里
});
module
說完了require做了些什么事,那么require觸發(fā)的module做了些什么呢?我們看看用法,先寫一個(gè)簡(jiǎn)單的導(dǎo)出模塊,寫好了模塊之后,只需要把需要導(dǎo)出的參數(shù),加入module.exports就可以了。
let count=0
function addCount(){
count++
}
module.exports={count,addCount}
然后根據(jù)require執(zhí)行代碼時(shí)需要加上的,那么實(shí)際上我們的代碼長(zhǎng)成這樣:
(function(exports, require, module, __filename, __dirname) {
let count=0
function addCount(){
count++
}
module.exports={count,addCount}
});
require的時(shí)候究竟module發(fā)生了什么,我們可以在vscode打斷點(diǎn):

根據(jù)這個(gè)斷點(diǎn),我們可以整理出:
黃色圈出來的時(shí)require,也就是我們調(diào)用的方法
紅色圈出來的時(shí)Module的工作內(nèi)容
Module._compile Module.extesions..js Module.load tryMouduleLoad Module._load Module.runMain
藍(lán)色圈出來的是nodejs干的事,也就是NativeModule,用于執(zhí)行module對(duì)象的。
我們都知道在JS中,函數(shù)的調(diào)用時(shí)棧stack的方式,也就是先近后出,也就是說require這個(gè)函數(shù)觸發(fā)之后,圖中的運(yùn)行時(shí)從下到上運(yùn)行的。也就是藍(lán)色框最先運(yùn)行。我把他的部分代碼扒出來,研究研究。
NativeModule原生代碼關(guān)鍵代碼,這一塊用于封裝模塊的。
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
等NativeModule觸發(fā)Module.runMain之后,我們的模塊加載開始了,我們按照從下至上的順序來解讀吧。
Module._load,就是新建一個(gè)module對(duì)象,然后將這個(gè)新對(duì)象放入Module緩存之中。
var module = new Module(filename, parent); Module._cache[filename] = module;
tryMouduleLoad,然后就是新建的module對(duì)象開始解析導(dǎo)入的模塊內(nèi)容
module.load(filename);
新建的module對(duì)象繼承了Module.load,這個(gè)方法就是解析文件的類型,然后分門別類地執(zhí)行
Module.extesions..js這就干了兩件事,讀取文件,然后準(zhǔn)備編譯
Module._compile終于到了編譯的環(huán)節(jié),那么JS怎么運(yùn)行文本?將文本變成可執(zhí)行對(duì)象,js有3種方法:
eval方法eval("console.log('aaa')")
new Function() 模板引擎
let str="console.log(a)"
new Function("aaa",str)
node執(zhí)行字符串,我們用高級(jí)的vm
let vm=require("vm")
let a='console.log("a")'
vm.runInThisContext(a)
這里Module用vm的方式編譯,首先是封裝一下,然后再執(zhí)行,最后返回給require,我們就可以獲得執(zhí)行的結(jié)果了。
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
因?yàn)樗械哪K都是封裝之后再執(zhí)行的,也就說導(dǎo)入的這個(gè)模塊,我們只能根據(jù)module.exports這一個(gè)對(duì)外接口來訪問內(nèi)容。
總結(jié)一下
這些代碼看的人真的很暈,其實(shí)主要流程就是require之后解析路徑,然后觸發(fā)Module這一個(gè)類,然后Module的_load的方法就是在當(dāng)前模塊中創(chuàng)建一個(gè)新module的緩存,以保證下一次再require的時(shí)候可以直接返回而不用再次執(zhí)行。然后就是這個(gè)新module的load方法載入并通過VM執(zhí)行代碼返回對(duì)象給require。
正因?yàn)槭沁@樣編譯運(yùn)行之后賦值給的緩存,所以如果export的值是一個(gè)參數(shù),而不是函數(shù),那么如果當(dāng)前參數(shù)的數(shù)值改變并不會(huì)引起export的改變,因?yàn)檫@個(gè)賦予export的參數(shù)是靜態(tài)的,并不會(huì)引起二次運(yùn)行。
CommonJs模塊和ES6模塊的區(qū)別
使用場(chǎng)景
CommonJS因?yàn)殛P(guān)鍵字的局限性,因此大多用于服務(wù)器端。而ES6的模塊加載,已經(jīng)有瀏覽器支持了這個(gè)特性,因此ES6可以用于瀏覽器,如果遇到不支持ES6語法的瀏覽器,可以選擇轉(zhuǎn)譯成ES5。
語法差異
ES6也是一種JavaScript的規(guī)范,它和CommonJs模塊的區(qū)別,顯而易見,首先代碼就不一樣,ES6的導(dǎo)入導(dǎo)出很直觀import和export。
| commonJS | ES6 | |
|---|---|---|
| 支持的關(guān)鍵字 | arguments,require,module,exports,__filename,__dirname | import,export |
| 導(dǎo)入 | const path=require("path") | import path from "path" |
| 導(dǎo)出 | module.exports = APP; | export default APP |
| 導(dǎo)入的對(duì)象 | 隨意修改 | 不能隨意修改 |
| 導(dǎo)入次數(shù) | 可以隨意require,但是除了第一次,之后都是從模塊緩存中取得 | 在頭部導(dǎo)入 |
** 大家注意了!劃重點(diǎn)!nodejs是CommonJS的親兒子,所以有些ES6的特性并不支持,比如ES6對(duì)于模塊的關(guān)鍵字import和export,如果大家在nodejs環(huán)境下運(yùn)行,就等著大紅的報(bào)錯(cuò)吧~**
加載差異
除了語法上的差異,他們引用的模塊性質(zhì)是不一樣的。雖然都是模塊,但是這模塊的結(jié)構(gòu)差異很大。
在ES6中,如果大家想要在瀏覽器中測(cè)試,可以用以下代碼:
//utils.js const x = 1; export default x
<script type="module"> import x from './utils.js'; console.log(x); export default x </script>
首先要給script一個(gè)type="module"表明這里面是ES6的模塊,而且這個(gè)標(biāo)簽?zāi)J(rèn)是異步加載,也就是頁(yè)面全部加載完成之后再執(zhí)行,沒有這個(gè)標(biāo)簽的話代碼不然無法運(yùn)行哦。然后就可以直接寫import和export了。
ES6模塊導(dǎo)入的幾個(gè)問題:
- 相同的模塊只能引入一次,比如x已經(jīng)導(dǎo)入了,就不能再?gòu)膗tils中導(dǎo)入x
- 不同的模塊引入相同的模塊,這個(gè)模塊只會(huì)在首次import中執(zhí)行。
- 引入的模塊就是一個(gè)值的引用,并且是動(dòng)態(tài)的,改變之后其他的相關(guān)值也會(huì)變化
- 引入的對(duì)象不可隨意斬?cái)噫溄?,比如我引入的count我就不能修改他的值,因?yàn)檫@個(gè)是導(dǎo)入進(jìn)來的,想要修改只能在count所在的模塊修改。但是如果count是一個(gè)對(duì)象,那么可以改變對(duì)象的屬性,比如count.one=1,但是不可以count={one:1}。
大家可以看這個(gè)例子,我寫了一個(gè)改變object值的小測(cè)試,大家會(huì)發(fā)現(xiàn)utils.js中的count初始值應(yīng)該是0,但是運(yùn)行了addCount所以count的值動(dòng)態(tài)變化了,因此count的值變成了2。
let count=0
function addCount(){
count=count+2
}
export {count,addCount}
<script type="module">
import {count,addCount} from './utils.js';
//count=4//不可修改,會(huì)報(bào)錯(cuò)
addCount()
console.log(count);
</script>
與之對(duì)比的是commonJS的模塊引用,他的特性是:
上一節(jié)已經(jīng)解釋了,模塊導(dǎo)出的固定值就是固定值,不會(huì)因?yàn)楹笃诘男薷亩淖?,除非不?dǎo)出靜態(tài)值,而改成函數(shù),每次調(diào)用都去動(dòng)態(tài)調(diào)用,那么每次值都是最新的了。
導(dǎo)入的對(duì)象可以隨意修改,相當(dāng)于只是導(dǎo)入模塊中的一個(gè)副本。
如果想要深入研究,大家可以參考下阮老師的ES6入門——Module 的加載實(shí)現(xiàn)。
CommonJS模塊總結(jié)
CommonJS模塊只能運(yùn)行再支持此規(guī)范的環(huán)境之中,nodejs是基于CommonJS規(guī)范開發(fā)的,因此可以很完美地運(yùn)行CommonJS模塊,然后nodejs不支持ES6的模塊規(guī)范,所以nodejs的服務(wù)器開發(fā)大家一般使用CommonJS規(guī)范來寫。
CommonJS模塊導(dǎo)入用require,導(dǎo)出用module.exports。導(dǎo)出的對(duì)象需注意,如果是靜態(tài)值,而且非常量,后期可能會(huì)有所改動(dòng)的,請(qǐng)使用函數(shù)動(dòng)態(tài)獲取,否則無法獲取修改值。導(dǎo)入的參數(shù),是可以隨意改動(dòng)的,所以大家使用時(shí)要小心。
以上所述是小編給大家介紹的CommonJS的模塊加載詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
找出字符串中出現(xiàn)次數(shù)最多的字母和出現(xiàn)次數(shù)精簡(jiǎn)版
找出字符串中出現(xiàn)次數(shù)最多的字母和出現(xiàn)次數(shù)精簡(jiǎn)版,有需求的朋友可以參考下2012-11-11
一個(gè)php+js實(shí)時(shí)顯示時(shí)間問題
本文給大家分享的是解決的php+js實(shí)時(shí)顯示時(shí)間問題,主要是自己當(dāng)時(shí)的理解有問題,也許大家有和我一樣的情況,這里分享給大家2015-10-10
js 連接數(shù)據(jù)庫(kù)如何操作數(shù)據(jù)庫(kù)中的數(shù)據(jù)
JS中怎么連接數(shù)據(jù)庫(kù),和提取數(shù)據(jù)庫(kù)中的數(shù)據(jù),本文將以此問題詳細(xì)介紹,需要的朋友可以了解下2012-11-11
一個(gè)不錯(cuò)的仿攜程自定義數(shù)據(jù)下拉選擇select
這是一個(gè)仿攜程自定義的數(shù)據(jù)下拉選擇select,對(duì)一些比較重要的參數(shù)進(jìn)行的描述,方便初學(xué)者2014-09-09
JavaScript之事件委托實(shí)例(附原生js和jQuery代碼)
下面小編就為大家?guī)硪黄狫avaScript之事件委托實(shí)例(附原生js和jQuery代碼)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
微信小程序scroll-view實(shí)現(xiàn)上拉加載數(shù)據(jù)重復(fù)的解決方法
這篇文章主要為大家詳細(xì)介紹了微信小程序scroll-view實(shí)現(xiàn)上拉加載數(shù)據(jù)重復(fù)的解決方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08

