在最初 js 被設(shè)計(jì)用來做一些表單校驗(yàn)的簡單功能赤赊,當(dāng)初的 js 只是用來作為頁面展示的一個(gè)補(bǔ)充笼吟。后來隨著 web 的發(fā)展丙号,相當(dāng)一部分業(yè)務(wù)邏輯前置到了前端進(jìn)行處理乏梁,js 的地位越來越重要芝囤,文件也越來越龐大似炎,為了將大的功能模塊進(jìn)行拆分成一個(gè)一個(gè)小的組成部分辛萍,但是拆分成小的 js 文件又帶來了新的挑戰(zhàn),由于 js 的加載和執(zhí)行順序在引入的時(shí)候就已經(jīng)決定了羡藐,這樣就需要充分考慮到各變量的作用范圍以及各變量之間的依賴關(guān)系贩毕。
就像上面這樣,a.js?會(huì)最先被執(zhí)行传睹,這樣如果在?b.js?中存在著與?a.js?同名的變量耳幢,就會(huì)發(fā)生覆蓋。同時(shí)如果在?c.js?中有使用到?a.js?聲明的變量就決定了?a.js必須在?c.js?上面被引入欧啤。這樣就存在這一種耦合睛藻,為了解決這一類問題 js 的模塊化應(yīng)運(yùn)而生。
commonjs
commonjs?隨著?nodejs?的誕生而面世邢隧,主要是用來解決服務(wù)端模塊化的問題店印,commonjs 對模塊做了如下規(guī)定
一個(gè) js 文件就是一個(gè)模塊,里面定義的變量都是私有的倒慧,對其他文件不可見
模塊內(nèi)的需要被導(dǎo)出的變量可以通過?exports?對象上的屬性進(jìn)行導(dǎo)出
使用?require?方法導(dǎo)入其他模塊的變量
所有的模塊加載都是同步的
同一個(gè)模塊可以被多次加載按摘,但是只有第一次被加載時(shí)會(huì)執(zhí)行模塊內(nèi)容,然后會(huì)緩存模塊
node 中的 commonjs 模塊
node?中一個(gè)文件就是一個(gè)模塊纫谅,各模塊之間的變量是無法互相訪問到的炫贤。
// a.jsconsta =1;
// b.jsconstb =2;console.log(a);// ReferenceError: a is not defined
在?b.js?中無法訪問到變量 a,如果需要使用 a 需要先導(dǎo)入模塊
// b.jsconsta =require('./a.js');console.log(a)// {}
這里還是無法訪問到 a 變量是因?yàn)槟K a 中沒有導(dǎo)出對應(yīng)的變量
// a.jsconsta =1exports.a= a// b.jsconsta =require('./a.js');console.log(a);// 1
node 模塊中的 module 對象
module?是?node?中的一個(gè)內(nèi)置對象付秕,是?Module?類的一個(gè)實(shí)例兰珍,?module?對象上有幾個(gè)重要屬性
module.id?模塊的標(biāo)識符。 通常是完全解析后的文件名
module.loaded?模塊是否已經(jīng)加載完成询吴,或正在加載中
exports.x=1;console.log(module.loaded)// false 還沒有加載完成setTimeout(() =>{console.log(module.loaded)// true},0)
module.exports?當(dāng)前模塊對外輸出的接口掠河,其他文件導(dǎo)入當(dāng)前模塊實(shí)際上就是在讀取當(dāng)前模塊的?module.exports?變量
除了?module.exports?之外,node?中還提供了一個(gè)內(nèi)置變量?exports猛计,它是?module.exports?的一個(gè)引用(可以理解成是一個(gè)快捷方式)唠摹,看一下?exports?和?module.exports?的關(guān)系
/**
* 實(shí)際的引用關(guān)系
* module.exports = {}
* exports = module.exports
*//**
* 一
* 這樣做的實(shí)際結(jié)果就是讓
* module.exports = {x: 1}
*/exports.x=1;// {x: 1}/**
* 二
* 同上
*/module.exports.x=1;// {x: 1}/**
* 三
* 雖然最終導(dǎo)出的內(nèi)容與上面兩種做法是
* 一樣的,但是這種做法改變了
* module.exports 的原始引用奉瘤,導(dǎo)
* 致了 exports 與 module.exports 的
* 聯(lián)系斷掉了勾拉,如果再使用 exports.y = 2
* 是沒有效果的
*/module.exports= {x:1};// {x: 1}exports.y=2;// 無效/**
* 四
* 與上面類似,改變了 exports 的引用
*/exports= {x:1};// 無效module.exports.y=2;// 2
node 模塊中的 require 方法
require?是?node?模塊中的內(nèi)置方法毛好,該方法用于導(dǎo)入模塊望艺,其函數(shù)簽名如下:
interfacerequire {/**
? * id? 模塊的名稱或路徑
? */(id:string):any}
require?方法上有幾個(gè)比較重要的屬性和方法
require.main?是?Module?的一個(gè)實(shí)例,表示當(dāng)前?node?進(jìn)程啟動(dòng)的入口模塊
require.resolve?是一個(gè)方法肌访,用來查詢指定的模塊的路徑找默,如果存在會(huì)返回模塊的路徑(如果是原生模塊,則只會(huì)返回原生模塊的名稱吼驶,例如?http)惩激,不存在則會(huì)報(bào)出錯(cuò)誤店煞,與?require?不同的是這個(gè)方法只會(huì)查找對應(yīng)的模塊路徑,不會(huì)執(zhí)行模塊中的代碼风钻,其函數(shù)簽名如下
interfaceRequireResolve{/**
? * request 指定要查找的模塊路徑
? * options.paths 從 paths 指定的路徑中進(jìn)行查找
? */(request: string,options: {paths: string[]}): string}
// /home/user/a.jsconsole.log(require.resolve('.b'));// /home/user/b.jsconsole.log(require.resolve('http'));// httpconsole.log(require.resolve('./index', {paths: ['/home/local/']}));// /home/local/index.js
require.resolve?方法與?require?解析文件路徑的方式是一樣的(后面會(huì)做介紹具體的解析過程)顷蟀,會(huì)優(yōu)先查看是否是原生模塊、然后會(huì)查看是否具有緩存骡技、然后才是更具不同的文件擴(kuò)展名進(jìn)行查找
require.cache?是一個(gè)對象鸣个,被引入的模塊將被緩存在這個(gè)對象中,可以手動(dòng)進(jìn)行刪除
require 本身的用法
require?可以通過傳入?string?類型的 id 作為入?yún)⒉茧琲d 可以是一個(gè)文件路徑或者是一個(gè)模塊名稱囤萤,路徑可以是一個(gè)相對路徑(以 ./ 或者 ../ 開頭)或者是一個(gè)絕對路徑(以 / 開頭)。相對路徑的方式比較簡單是趴,會(huì)以當(dāng)前文件的?__dirname?作為基礎(chǔ)路徑計(jì)算出絕對路徑涛舍,無論是相對路徑還是絕對路徑都可以是文件或者文件夾。
i. 文件加載規(guī)則
LOAD_AS_FILE(X)LOAD_AS_FILE1.是否存在 X 文件唆途,是則優(yōu)先加載 X2.否則會(huì)加載 X.js3.否則會(huì)加載 X.json4.否則會(huì)加載 X.node
ii. 文件夾加載規(guī)則
LOAD_AS_DIRECTORY(X)LOAD_AS_DIRECTORY1.是否存在`X/package.json`富雅,是則繼續(xù)? ? a. `package.json` 是否有 `main` 字段,無則執(zhí)行 2肛搬,是則執(zhí)行 b
? ? b. 加載 `(X + main)` 文件没佑,規(guī)則: `LOAD_AS_FILE(X + main)` ,無則繼續(xù)執(zhí)行 c
? ? c. 加載 `(X + main)/index`温赔,規(guī)則: `LOAD_AS_FILE((X + main)/index)`图筹,無則拋出錯(cuò)誤
2. 否則會(huì)執(zhí)行去查找 `X/index`,規(guī)則: `LOAD_AS_FILE(X/index)`
iii. 模塊名稱加載規(guī)則
id 作為模塊名稱會(huì)遵守如下優(yōu)先級規(guī)則進(jìn)行模塊查找:
加載內(nèi)置模塊
加載當(dāng)前目錄下?node_modules?文件夾中的模塊
加載父級目錄下?node_modules?文件夾中的模塊让腹,一直到最頂層
模塊緩存
模塊在第一次被加載之后會(huì)緩存,多次調(diào)用同一個(gè)模塊只會(huì)讓模塊執(zhí)行一次扣溺。
// a.jsmodule.exports= {name:'張三'}// b.jsrequire('./a.js')// {name: '張三'}require('./a.js').age=18require('./a.js')// {name: '張三', age: 18}
最后一個(gè)?require('./a.js')?會(huì)輸出?{name: '張三', age: 18}?則說明?a.js?模塊只執(zhí)行了一次骇窍,返回的還是最早被緩存的對象。如果要強(qiáng)制重新執(zhí)行被引用的模塊代碼锥余,可以通過刪除緩存的方式
// a.jsmodule.exports= {name:'張三'}// b.jsrequire('./a.js')// {name: '張三'}require('./a.js').age=18require('./a.js')// {name: '張三', age: 18}deleterequire.cache[require.resolve('./a')]require('./a.js')// {name: '張三'}
上面的例子還能說明模塊的緩存是基于文件路徑進(jìn)行的腹纳,只要在被加載時(shí)路徑不一致同一個(gè)模塊也會(huì)執(zhí)行兩次
循環(huán)依賴
要說弄清楚這個(gè)問題需要先了解 node 中模塊加載機(jī)制,在?commonjs?模塊體系中?require?加載的是一個(gè)對象的副本驱犹,實(shí)際也就是?module.exports?所指向的變量嘲恍,所以除非是存在引用類型的變量否則模塊內(nèi)部的變化是影響不到外部的。舉個(gè)例子說明這個(gè):
// b.jsletcount =1letcountObj = {count:10}module.exports= {? count,? countObj,setCount(newVal) {? ? count = newVal? },setCountObj(newVal) {? ? countObj.count= newVal? }}// a.jsconstmoduleB =require('./b.js')console.log(moduleB.count)// 1moduleB.setCount(2)console.log(moduleB.count)// 1console.log(moduleB.countObj.count)// 10moduleB.setCountObj(20)console.log(moduleB.countObj.count)// 20
上面的例子說明了?require?的結(jié)果實(shí)際是?module.exports?的一個(gè)副本雄驹,按照這樣的思路循環(huán)加載的情況下佃牛,也就會(huì)讀取已經(jīng)存在?module.exports?上的屬性,如果還存在部分屬性未掛在到?module.exports?上則會(huì)讀取不到医舆。
// a.jsconsole.log('a 開始');exports.done=false;constb =require('./b.js');console.log('在 a 中俘侠,b.done = %j', b.done);exports.done=true;console.log('a 結(jié)束');// b.jsconsole.log('b 開始');exports.done=false;consta =require('./a.js');console.log('在 b 中象缀,a.done = %j', a.done);exports.done=true;console.log('b 結(jié)束');// main.jsconsole.log('main 開始');consta =require('./a.js');constb =require('./b.js');console.log('在 main 中,a.done=%j爷速,b.done=%j', a.done, b.done);
當(dāng)?main.js?加載?a.js?時(shí)央星,?a.js?又加載?b.js。 此時(shí)惫东,?b.js?會(huì)嘗試去加載?a.js莉给。 為了防止無限的循環(huán),會(huì)返回一個(gè)?a.js?的?exports?對象的 未完成的副本 給?b.js?模塊廉沮。 然后?b.js?完成加載颓遏,并將?exports?對象提供給?a.js?模塊。
當(dāng) main.js 加載這兩個(gè)模塊時(shí)废封,它們都已經(jīng)完成加載州泊。 因此,該程序的輸出會(huì)是:
main 開始a 開始b 開始在 b 中漂洋,a.done =falseb 結(jié)束在 a 中遥皂,b.done =truea 結(jié)束在 main 中,a.done=true刽漂,b.done=true