nodejs端實現模塊化的方式一般是經過commonjs,使用模塊化能夠複用js代碼,使得邏輯結構更爲清晰。javascript
commonjs的語法規則以下
經過 module.exports 或者 exports 導出,經過 require函數來導入java
// a.js 導出內容 const name = 'alice' const age = 16 module.exports = { name, age } // b.js 導入 const obj = require('./a.js') console.log(obj) // { name: 'alice', age: 16}
module.exports和exports 導出不一樣的書寫方式node
// a.js const name = 'alice' const age = 16 exports.age = age module.exports.name = name // b.js const obj = require('./a.js') console.log(obj) // { name: 'kiki', age: 16 }
二者的不一樣之處在於,即便經過 exports.xxx 導出,其實最終也是經過 module.exports 這樣的方式進行導出的,由於在node內部,初始化時先將一個空對象賦值給 exports,而後再將exports賦值給module.exports,因此經過 exports.xxx 這樣的形式就至關於在 module.exports 這個對象裏面追加子元素
算法
若是直接給module.exports賦值爲一個新的對象,那麼結果就不同了json
// a.js const name = 'alice' const age = 16 const hobby = 'singing' exports.name = name exports.age = age module.exports = { hobby } // b.js const obj = require('./a.js') console.log(obj) // { hobby: 'singing' }
從新將一個對象賦值給了 module.exports,module.exports 指向的內存空間已經和 exports 不是同一個指向,因此最終的導出只有 module.exports 部分
瀏覽器
爲何說是exports賦值給module.exports而不是反向賦值呢,一方面能夠從源碼中看到賦值的過程,另外一方面改變一下導出方式也能看到,若是 require 最終導入的內容由 exports 決定的話,那麼此時輸出應該爲一個函數模塊化
// a.js const sayHi = function(){ console.log('hi') } exports = { sayHi } // b.js const obj = require('./a.js') console.log(obj) // {}
此時只輸出了一個空對象,因此說 require的導出內容並非 exports 而是 module.exports。
初始化的時候 exports 與 module.exports 都是指向了一個空對象的內存地址,當 exports 直接添加空對象的屬性時,內存地址沒有發生改變,modulex.exports 能隨着exports的賦值導出的內容發生變化。
可是這裏當 exports 從新指向了另外的對象時,module.exports 仍是指向原來的那個對象,module.exports沒有發生變化,因此導出的內容仍是空對象。
函數
瞭解完了 導出 這一部分,咱們來看看 require 導入,經過require來查找資源時,會有以下規則性能
一、導入是內置模塊,好比 fs、http, 此時就會直接查找內置的模塊 二、導入的模塊包含路徑, 好比 ./ 或 ../ 或 / ./ 表示當前路徑,../ 表示上一層路徑,/ 表示電腦的根目錄 若是有路徑時,就會按照路徑去查找 此時分爲兩種狀況,一種是當成文件,一種是文件夾 (1) 文件 若是文件寫了後綴名,就會查找有該後綴名的文件,若是沒有後綴名,就會按照 文件/文件.js / 文件.json/文件.node 的次序查找 (2) 文件夾 依次找查找文件夾的 index.js / index.json / index.node 若是經過路徑沒有找到,則會報錯 Not Find 三、導出既不是內置模塊也沒有路徑 那麼會從當前目錄的 node_modules 開始,一層一層往上查找有沒有對應的導入文件,若是沒有找到 則報錯 Not Find
那麼require的模塊加載過程是怎麼樣的呢~
首先來講, 只要使用了 require 函數來加載資源,必定會被執行一次ui
// a.js let name = 'kiki' exports.name = name console.log('a.js文件被執行') // b.js require('./a.js') // a.js文件被執行
若是多個文件都導入同一個文件,也並不會讓該文件屢次加載,在module內有一個 loaded 字段,來判斷該資源是否被加載。
下圖在 b.js 中打印的 module,能夠看到子元素 a.js文件的loaded 已經變成了 true,由於打印語句在 require 以後,而 commonjs 是同步執行,因此 a.js 已經被加載完成,而打印的時候 b.js 尚未加載完成,因此loaded爲false
若是屢次循環調用也不須要擔憂,若是出現瞭如下嵌套調用的狀況,會按照深度優先算法來對文件進行執行。
首先從 main 對應的第一個頂點開始,一層一層往下找,找到底了以後,再往上一層查找,把上一層的元素查找完以後,再往上一層查找,也就是 main-->aaa-->ccc-->ddd--->eee。
此時eee沒有指向的頂點,就退回到ddd,ddd除了eee也沒有指向的頂點,再退回到ccc,依此類推,一直退到main,發現main還指向了另一個頂點 bbb,因此執行bbb,bbb指向ccc和eee,但由於這兩個已經加載過,因此不會從新加載,最後的執行順序爲
main aaa ccc ddd eee bbb
此外,commonjs的執行還有幾個特色
一、同步加載,每次執行js代碼都是須要將js文件下載下來,服務端處理文件一般都是執行本地文件,不會對性能形成很大的影響,可是若是用於瀏覽器端,同步執行代碼就會對後續的js執行形成阻塞,使加載資源的時間變得更長,因此commonjs大多都被用在服務端。
// a.js let name = 'kiki' exports.name = name console.log('a.js文件被執行') // b.js require('./a.js') console.log('b.js文件被執行') // a.js文件被執行 b.js文件被執行
二、運行時解析,這裏就要說到v8引擎的執行流程,簡單來講js的執行須要經過兩個階段,首先須要將javascript代碼解析成字節碼,而後再經過編譯器執行,運行時解析就意味當函數調用以後,纔會執行commonjs的代碼,導入導出就可使用一些條件判斷語句或者動態的路徑
// a.js let name = 'kiki' module.exports = { name } // b.js const path = './a.js' const flag = true if(flag){ const { name } = require(path) console.log(name) // kiki }
三、module.exports 與 require 的是同一個對象,也就是說若是直接更改 module.exports.xxx 的時候,require 的內容也會發生更改,修改require,module.exports的內容也會變化,下面演示一下修改導出的內容
// a.js let name = 'kiki' setTimeout(()=>{ module.exports.name = 'hi' },1000) module.exports = { name } // b.js const obj = require('./a.js') console.log(obj) setTimeout(()=>{ console.log(obj) }, 2000) // 執行順序爲 { name: 'kiki' } { name: 'hi' }
以上就是commonjs在nodejs中的使用詳解,commonjs是node實現模塊化中很是重要的一部份內容,把它理解透才能更好的應用~ 下一篇會介紹在瀏覽器端經常使用的模塊化方式 es module