談談對模塊化的理解

重要的模塊化規範有幾個:commonjs,ES6模塊機制,AMD,CMD。因爲業務中一直接觸的都是Vue+webpack+babel架構的項目,在封裝代碼時用的比較的多仍是ES6規範,對其餘模塊化規範不熟悉,所以在這裏記錄一下學習過的模塊化知識。javascript


CommonJS

模塊化的目的在於營造安全封閉的做用域、且具備易於引用接口,按個人理解可分爲模塊定義、模塊引入兩部分。java

在模塊中存在着一個module對象,它表明着模塊自己,將須要導出的api掛載於其中的exports屬性上便可以定義導出的接口;CommonJS規範中存在require()方法,用於接受模塊標識,引入某個模塊到當前的上下文。node

1. 模塊定義


要理解模塊如何定義,那必需要先理解module對象。在Node中,每個文件模塊都是一個對象,即module對象。它的定義以下:webpack

function Module(id, parent){
    this.id = id    //模塊標識符
    this.exports = {}    //模塊對外輸出的值
    this.parent = parent    //調用該模塊的模塊。parent爲null時意味着模塊爲入口模塊
    if(parent && parent.children){
        parent.children.push(this)
    }    
    this.filename = filename    //文件名
    this.loaded = false    //是否已加載
    this.children = []    //表示該模塊調用的其餘模塊
}

定義模塊的目的其實在於定義輸出的值。寫法很是簡單,舉個?git

function sayHello(){
    console.log('hello')
}
module.exports = sayHello    //或exports.sayHello = sayHello

爲了方便導出接口,Node還定義了一個exports變量,但有個容易踩的坑是,exports只是一個引用,原本指向module.exports,假如只是給exports變量賦值則exports變量會失去對module.exports的指向。說到底,必須對module.exports定義接口才能真正導出值。github

先說解決方法,常見的寫法爲:web

exports = module.exports = sayHello
//或嚴格地只給exports變量添加屬性
exports.sayHello = sayHello

再舉個例子說一下犯錯的具體場景:json

//a.js
exports.name = 'kent'
exports.sayHi = function(){
    console.log('hi')
}

console.log(module)// { exports: { name: 'kent', sayHi: function(){ console.log('hi') } } }

//假如給exports從新賦值 =_=
exports = {
    name: 'nicolas',
    sayBye: function(){
        console.log('bye')
     }
}

//module中的exports屬性不會有任何變化
console.log(module)// { exports: { name: 'kent', sayHi: function(){ console.log('hi') } } }
console.log(exports)// { name: 'nicolas', sayBye: function(){ console.log('bye') } }
//b.js
//所以require的時候讀取的name仍然爲kent
var person = require('a.js')
console.log(person.name)//kent

具體的緣由也能夠從模塊機制中看出來api

function require(...) {
  var module = { exports: {} };
  ((module, exports) => {
    // Your module code here. In this example, define a function.
    function some_func() {};
    exports = some_func;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = some_func;
    // At this point, the module will now export some_func, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}

2. 模塊引入


模塊引入的語法也很是簡單。上一節也簡單提過。這裏再舉個?緩存

//book.js
exports.name = 'javascript'
exports.logName = function(){
    console.log('javascript')
}
//main.js
var book = require('./book.js')//require的參數即模塊標識符
console.log(book.name)//'javascript'
book.logName()//'javascript'

下面詳情談談模塊引入經歷哪些步驟。但在此以前須要先了解兩個概念:核心模塊與文件模塊。

在Node中,有一部分模塊由Node提供,稱之爲核心模塊。在Node進程啓動的時候,核心模塊就直接加載至內存中。所以引入核心模塊只須要走路徑分析一個步驟,其加載速度最快。

另外一部分則是運行時動態加載,常見的有用戶定義帶路徑標識符的模塊,或自定義模塊(如三方提供的包)。這類模塊須要完整地走完如下三個步驟:路徑分析、文件定位與編譯執行。

①路徑分析:
路徑分析能夠理解爲模塊標識符的分析。模塊標識符在Node中主要有:

·核心模塊,如:http, fs等等;
    ·以"./"或"../"開頭的相對路徑模塊,相對於當前的目錄位置;
    ·以"/"開頭的絕對路徑模塊;
    ·非路徑形式的文件模塊,與核心模塊的標識符相似。Node會搜索各級的node_modules目錄。

· 核心模塊:核心模塊通過路徑分析以後會直接加載。須要注意的是,自定義的文件模塊不能與核心模塊標識符相同,要不更換不一樣的標識符要麼使用相對路徑或絕對路徑標識符。

· 路徑形式的文件模塊:在分析文件模塊的時候,require方法將會把路徑轉換爲真實路徑並以此爲索引編譯模塊並存放到緩存中(緩存加載將在下文介紹)。

· 非路徑形式的文件模塊(自定義模塊):自定義模塊的路徑分析在咱們引用三方庫的時候常常會碰到。這類非路徑形式的文件模塊加載時將會以模塊路徑爲線索逐級搜索。舉個? :

//在"/Users/zhazheng/Documents/www"下新建一個module_path.js

//module_path.js
console.log(module.paths)

//再執行module_path.js
node module_path

//得出如下log
[ '/Users/zhazheng/Documents/www/node_modules',
  '/Users/zhazheng/Documents/node_modules',
  '/Users/zhazheng/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

可見,這類模塊會從當前文件目錄往上逐級遞歸直到根目錄下的node_modules目錄。所以這類模塊的路徑分析是最費時的。

②文件定位:文件定位主要包括文件擴展名分析、目錄和包的處理。

·文件擴展名分析:分析標識符的過程當中出現不包含文件擴展名的狀況很是常見。在標識符不包含文件擴展名的狀況下,Node會依次嘗試如下三種擴展名:.js、.json、.node。因爲嘗試解析的過程是同步阻塞進行的,所以大量的分析文件擴展名會產生性能問題,這種狀況下能夠嘗試添加擴展名或充分利用緩存加載的優點。

·目錄分析與包的處理: 假如分析完擴展名後仍然沒有找到對應的文件而只得出一個目錄,那麼Node會將此目錄當作一個包來處理。首先會查找當前目錄下是否有package.json文件,假若有則檢查是否具備main屬性(main屬性即指向入口文件)。假如沒有package.json文件或package.json中不具有main屬性,那麼Node則按index爲默認的文件名,最後再重複「文件擴展名分析」這個步驟。

3.緩存加載


事實上Node的模塊,不管是核心模塊仍是文件模塊,第一次加載以後都會被緩存。require()方法將會對二次加載的模塊進行緩存。所以假若有屢次加載模塊的需求,那麼就須要記得先從緩存中刪除模塊。

緩存均保存在require.cache對象中,須要刪除單個模塊或所有模塊的緩存能夠這樣寫:

//刪除單個模塊緩存
delete require.cache[moduleName]

//刪除所有模塊緩存
Object.getOwnPropertyNames(require.cache).forEach(key => {
    delete require.cache[key]
})

固然,通常狀況下緩存是能夠帶來性能優點的。對於路徑套得很是深的自定義文件模塊來講尤甚。

4.循環加載


循環加載是避免不了的問題。在Node中須要瞭解一下循環加載的表現。首先要理解的是,require是一個同步加載的過程,讀取的接口僅僅是指向exports對象中的屬性,舉個? :(如下三個模塊均在同一目錄下)

//a.js
exports.name = 'a1'
console.log(`a.js, ${require('./b.js').name}`)
exports.name = 'a2'
//b.js
exports.name = 'b1'
console.log(`b.js, ${require('./a.js').name}`)
exports.name = 'b2'
//main.js
console.log(`main.js, ${require('./a.js').name}`)
console.log(`main.js, ${require('./b.js').name}`)

nvm run node而後.load main.js得出如下的結果

b.js, a1
a.js, b2//這兩行結果應該大體能夠理解兩個模塊的require方法發生了什麼
main.js a2
main.js b2

再次執行.load main.js會讀取緩存結果

main.js a2
main.js b2

循環加載示例代碼可到個人github查看

AMD

...未完待續
相關文章
相關標籤/搜索