nodejs端模塊化方式comomjs詳解

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

相關文章
相關標籤/搜索