深刻淺出Node.js(一) 模塊機制

模塊分類

Node.js有兩種模塊javascript

  1. 核心模塊
    部分核心模塊已經被直接加載進內存中,路徑分析 編譯執行的步驟能夠省略 而且在路徑分析中優先被判斷,因此加載速度最快
  2. 文件模塊
    運行時動態加載,因此須要完整的路徑分析 文件定位和 編譯執行過程,因此速度比核心模塊慢

實現「模塊」功能的奧妙就在於JavaScript是一種函數式編程語言,它支持閉包。若是咱們把一段JavaScript代碼用一個函數包裝起來,這段代碼的全部「全局」變量就變成了函數內部的局部變量。java

var s = 'Hello';
var name = 'world';

console.log(s + ' ' + name + '!');
(function() {
    var s = 'Hello';
    var name = 'world';
    
    console.log(s + ' ' + name + '!');
})()

這樣一來,原來的全局變量s如今變成了匿名函數內部的局部變量。若是Node.js繼續加載其餘模塊,這些模塊中定義的「全局」變量s也互不干擾。node

因此,Node利用JavaScript的函數式編程的特性,垂手可得地實現了模塊的隔離。編程

模塊緩存機制

Nodejs對加載過的模塊 會進行緩存,以減小二次引入時的開銷,引入模塊時會優先從緩存中查找,Node緩存的是編譯和執行以後的對象json

緩存形式: key-value的形式,以真實路徑做爲key,以編譯執行後的結果做爲value 放在緩存中(Module._cache對象中)(二次加載速度更快)
打印rquire.cache 能夠看到緩存的對象緩存

模塊的循環引用

先說結論,因爲Node.js會緩存加載過的模塊,全部模塊的循環依賴並不會引發無限循環引用。舉個例子:閉包

a.js文件下編程語言

console.log('a starting');
exports.done = false

const b = require('./b.js')
console.log('in a, b done = %j', b.done);

exports.done = true
console.log('a done');

b.js文件下函數式編程

console.log('b starting');
exports.done = false

// 這裏導入的是a未執行完的副本
const a = require('./a.js')
console.log('in b, a done = %j', a.done);

exports.done = true
console.log('b done');

main.js文件下函數

console.log('main starting');
const a = require('./a')
const b = require('./b')

console.log('in main.js, a done = %j, b done = %j', a.done, b.done);

整個詳細的過程分析以下:

  1. node main.js
  2. require a.js,load a.js,輸出「a starting「
  3. a: exports.done = false,require b.js,load b.js
  4. 輸出」b starting「,b: exports.done = false
  5. require a.js, 因爲a.js沒有執行完,將未完成的副本導出,因此 a = {done: false}
  6. 輸出in b, a.done = false
  7. b: exports.done = true,輸出b done,b.js執行完畢,返回a.js繼續執行
  8. b = { done: true },輸出in a, b.done = true,輸出a done
  9. a.js 執行完畢,a = { done: true } ,返回 main.js 繼續執行,require b.js
  10. 因爲 b.js 已經被執行完畢,緩存中拿值,如今 a = { done: true }, b = { done: true }
  11. 輸出in main, a.done = true, b.done = true

因而可知,Node.js對已加載過的模塊進行緩存,解決了循環引用的問題,在二次加載時直接從緩存中取,提升了加載速度。

路徑分析和文件定位

咱們須要瞭解,自定義模塊是動態加載的(運行時加載),在首次加載時,要通過路徑分析、文件定位、編譯執行的過程。

在分析路徑模塊時,require()方法會去查找真實的路徑

  1. 若是沒有擴展名,會按照分析順序:.js > .node > .json 依次進行匹配
  2. 若是沒有查找到對應的文件,可是獲得的是一個目錄,那麼會被當成一個包來處理
    優先查找package.jsonmain屬性指定的文件名進行定位 > index.js > index.node > index.json 依次匹配

模塊的編譯

編譯和執行是引入文件模塊的最後一個階段。這裏只講對.js文件的編譯,經過fs模塊同步讀取文件後進行編譯,每一個編譯成功的模塊都會以它的真實路徑做爲索引緩存在Module._cache對象上,以提升二次引入的性能。

編譯過程當中,Node會對獲取到的文件進行頭尾包裝

(function(module, exports, require, __filename, __dirname) {
    
})

這樣每一個模塊之間都進行了做用域隔離 也解釋了咱們沒有在模塊文件中定義module、exports、__filename、 __dirname這些變量卻能夠使用它們的緣由。

module.exports和exports的區別

Node.js在執行一個javascript文件時,會生成一個moduleexports對象, module還有一個exports屬性,module.exportsexports指向同一個引用
二者的根本區別是:
exports返回的是模塊函數,module.exports返回的是模塊對象自己
舉個例子:
a.js文件下

let sayHello = function() {
    console.log('hello');
}
exports.sayHello = sayHello

b.js文件下

// 這樣使用會報錯
const sayHello = require('./a')
sayHi()

// 正確的方式
const func = require('./a')
func.sayHello() // hello

新建c.js文件

let sayHello = function() {
    console.log('hello');
}
// 1方式導出
module.exports.sayHello = sayHello
// 2方式導出
module.exports = sayHello

b.js中引入

// 1方式的
const func = require('./a')
func.sayHello() // hello

// 2方式的
const sayHello = require('./a')
sayHello() // hello

能夠看出,1方式的導出跟exports的導出,在引入時的方式是一致的

module.exports.sayHello = sayHello
等同於
module.exports = {
   sayHello: sayHello
}
也等同於
exports.sayHello = sayHello

還有一個注意的點是:執行require()方法時,引入的是module.exports導出的內容。

// d.js文件下:
exports = {
    a: 200
}
module.exports = {
    a: 100
}

// b.js引入
const value = require('./d')
console.log('value', value); // {a: 100}

從上面能夠看出,其實require導出的內容是module.exports的指向的內存塊內容,並非exports的。

// 若是d.js文件變成
exports = {
    a: 200
}

// b.js引入
const value = require('./d')
console.log('value', value); // {}

能夠看到打印出來的值爲{} 那是由於exports原本指向跟module.exports同一個引用,如今exports = {a: 200} exports指向了另外一個內存地址,將與module.exports脫離關係,默認module.eports={}

總結

  • exports 是 module.exports 的一個引用
  • module.exports 初始化是一個{},exports 也是這個{}
  • require 引用返回的是 module.exports,而不是 exports
  • exports.xxx = xxxx 至關於在導出對象上直接添加屬性或者修改屬性值,在調用模塊直接可見
  • exports = xxx 爲 exports 從新分配內存,將脫離 module.exports ,二者無關聯。調用模塊將不能訪問。

參考:
Node 模塊機制不徹底指北

相關文章
相關標籤/搜索