CommonJS 和 ES6 Module 究竟有什么區(qū)別歹叮?

CommonJS 和 ES6 Module 究竟有什么區(qū)別?

作為前端開發(fā)者辙芍,你是否也曾有過疑惑啡彬,為什么可以代碼中可以直接使用 require 方法加載模塊,為什么加載第三方包的時候 Node 會知道選擇哪個文件作為入口故硅,以及常被問到的庶灿,為什么 ES6 Module export 基礎(chǔ)數(shù)據(jù)類型的時候會有【引用類型】的效果?

帶著這些疑問和好奇吃衅,希望閱讀這篇文章能解答你的疑惑往踢。

CommonJS 規(guī)范

在 ES6 之前,ECMAScript 并沒有提供代碼組織的方式徘层,那時候通常是基于 IIFE 來實現(xiàn)“模塊化”峻呕,隨著 JavaScript 在前端大規(guī)模的應(yīng)用利职,以及服務(wù)端 Javascript 的推動,原先瀏覽器端的模塊規(guī)范不利于大規(guī)模應(yīng)用瘦癌。于是早期便有了 CommonJS 規(guī)范猪贪,其目標(biāo)是為了定義模塊,提供通用的模塊組織方式讯私。

模塊定義和使用

在 Commonjs 中热押,一個文件就是一個模塊。定義一個模塊導(dǎo)出通過 exports 或者 module.exports 掛載即可妄帘。

exports.count = 1;

導(dǎo)入一個模塊也很簡單楞黄,通過 require 對應(yīng)模塊拿到 exports 對象。

 const counter = require('./counter');
 console.log(counter.count);

CommonJS 的模塊主要由原生模塊 module 來實現(xiàn)抡驼,這個類上的一些屬性對我們理解模塊機制有很大幫助鬼廓。

 Module {
  id: '.', // 如果是 mainModule id 固定為 '.',如果不是則為模塊絕對路徑
  exports: {}, // 模塊最終 exports
  filename: '/absolute/path/to/entry.js', // 當(dāng)前模塊的絕對路徑
  loaded: false, // 模塊是否已加載完畢
  children: [], // 被該模塊引用的模塊
  parent: '', // 第一個引用該模塊的模塊
  paths: [ // 模塊的搜索路徑
  '/absolute/path/to/node_modules',
  '/absolute/path/node_modules',
  '/absolute/node_modules',
  '/node_modules'
  ]
 }

require 從哪里來致盟?

在編寫 CommonJS 模塊的時候碎税,我們會使用 require 來加載模塊,使用 exports 來做模塊輸出馏锡,還有 module雷蹂,filename, dirname 這些變量,為什么它們不需要引入就能使用杯道?

原因是 Node 在解析 JS 模塊時匪煌,會先按文本讀取內(nèi)容,然后將模塊內(nèi)容進行包裹党巾,在外層裹了一個 function萎庭,傳入變量。再通過 vm.runInThisContext 將字符串轉(zhuǎn)成 Function形成作用域齿拂,避免全局污染驳规。

let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
 };
 ?
 const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
 ];

于是在 CommmonJS 的模塊中可以不需要 require,直接訪問到這些方法署海,變量吗购。

參數(shù)中的 module 是當(dāng)前模塊的的 module 實例(盡管這個時候模塊代碼還沒編譯執(zhí)行),exports 是 module.exports 的別名砸狞,最終被 require 的時候是輸出 module.exports 的值捻勉。require 最終調(diào)用的也是 Module._load 方法。filename刀森,dirname 則分別是當(dāng)前模塊在系統(tǒng)中的絕對路徑和當(dāng)前文件夾路徑踱启。

模塊的查找過程

開發(fā)者在使用 require 時非常簡單,但實際上為了兼顧各種寫法,不同類型的模塊禽捆,node_modules packages 等模塊的查找過程稍微有點麻煩笙什。

首先,在創(chuàng)建模塊對象時胚想,會有 paths 屬性琐凭,其值是由當(dāng)前文件路徑計算得到的,從當(dāng)前目錄一直到系統(tǒng)根目錄的 node_modules浊服⊥城可以在模塊中打印 module.paths 看看。

 [ 
  '/Users/evan/Desktop/demo/node_modules',
  '/Users/evan/Desktop/node_modules',
  '/Users/evan/node_modules',
  '/Users/node_modules',
  '/node_modules'
 ]

除此之外牙躺,還會查找全局路徑(如果存在的話)

 [
  execPath/../../lib/node_modules, // 當(dāng)前 node 執(zhí)行文件相對路徑下的 lib/node_modules
  NODE_PATH, // 全局變量 NODE_PATH
  HOME/.node_modules, // HOME 目錄下的 .node_module
  HOME/.node_libraries' // HOME 目錄下的 .node-libraries
 ]

按照官方文檔給出的查找過程已經(jīng)足夠詳細愁憔,這里只給出大概流程。

 從 Y 路徑運行 require(X)
 ?
 1. 如果 X 是內(nèi)置模塊(比如 require('http'))
   a. 返回該模塊孽拷。
   b. 不再繼續(xù)執(zhí)行吨掌。
 ?
 2. 如果 X 是以 '/' 開頭、
  a. 設(shè)置 Y 為 '/'
 ?
 3. 如果 X 是以 './' 或 '/' 或 '../' 開頭
  a. 依次嘗試加載文件脓恕,如果找到則不再執(zhí)行
  - (Y + X)
  - (Y + X).js
  - (Y + X).json
  - (Y + X).node
  b. 依次嘗試加載目錄膜宋,如果找到則不再執(zhí)行
  - (Y + X + package.json 中的 main 字段).js
  - (Y + X + package.json 中的 main 字段).json
  - (Y + X + package.json 中的 main 字段).node
   c. 拋出 "not found"
 4. 遍歷 module paths 查找,如果找到則不再執(zhí)行
 5. 拋出 "not found"

模塊查找過程會將軟鏈替換為系統(tǒng)中的真實路徑炼幔,例如 lib/foo/node_moduels/bar 軟鏈到 lib/bar秋茫,bar 包中又 require('quux'),最終運行 foo module 時乃秀,require('quux') 的查找路徑是 lib/bar/node_moduels/quux 而不是 lib/foo/node_moduels/quux肛著。

模塊加載相關(guān)

MainModule

當(dāng)運行 node index.js 時,Node 調(diào)用 Module 類上的靜態(tài)方法 _load(process.argv[1])加載這個模塊跺讯,并標(biāo)記為主模塊枢贿,賦值給 process.mainModule 和 require.main,可以通過這兩個字段判斷當(dāng)前模塊是主模塊還是被 require 進來的抬吟。

CommonJS 規(guī)范是在代碼運行時同步阻塞性地加載模塊萨咕,在執(zhí)行代碼過程中遇到 require(X)時會停下來等待统抬,直到新的模塊加載完成之后再繼續(xù)執(zhí)行接下去的代碼火本。

雖說是同步阻塞性,但這一步實際上非炒辖ǎ快钙畔,和瀏覽器上阻塞性下載、解析金麸、執(zhí)行 js 文件不是一個級別擎析,硬盤上讀文件比網(wǎng)絡(luò)請求快得多。

img

緩存和循環(huán)引用

文件模塊查找挺耗時的,如果每次 require 都需要重新遍歷文件夾查找揍魂,性能會比較差桨醋;還有在實際開發(fā)中,模塊可能包含副作用代碼现斋,例如在模塊頂層執(zhí)行 addEventListener 喜最,如果 require 過程中被重復(fù)執(zhí)行多次可能會出現(xiàn)問題。

CommonJS 中的緩存可以解決重復(fù)查找和重復(fù)執(zhí)行的問題庄蹋。模塊加載過程中會以模塊絕對路徑為 key, module 對象為 value 寫入 cache瞬内。在讀取模塊的時候會優(yōu)先判斷是否已在緩存中,如果在限书,直接返回 module.exports虫蝶;如果不在,則會進入模塊查找的流程倦西,找到模塊之后再寫入 cache能真。

// a.js
 module.exports = {
  foo: 1,
 };
 ?
 // main.js
 const a1 = require('./a.js');
 a1.foo = 2;
 ?
 const a2 = require('./a.js');
 ?
 console.log(a2.foo); // 2
 console.log(a1 === a2); // true

以上例子中,require a.js 并修改其中的 foo 屬性扰柠,接著再次 require a.js 可以看到兩次 require 結(jié)果是一樣的舟陆。

模塊緩存可以打印 require.cache 進行查看。

 { 
  '/Users/evan/Desktop/demo/main.js': 
  Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/evan/Desktop/demo/main.js',
  loaded: false,
  children: [ [Object] ],
  paths: 
  [ '/Users/evan/Desktop/demo/node_modules',
  '/Users/evan/Desktop/node_modules',
  '/Users/evan/node_modules',
  '/Users/node_modules',
  '/node_modules'
  ]
  },
  '/Users/evan/Desktop/demo/a.js': 
  Module {
  id: '/Users/evan/Desktop/demo/a.js',
  exports: { foo: 1 },
  parent: 
  Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/evan/Desktop/demo/main.js',
  loaded: false,
  children: [Array],
  paths: [Array] },
  filename: '/Users/evan/Desktop/demo/a.js',
  loaded: true,
  children: [],
  paths: 
  [ '/Users/evan/Desktop/demo/node_modules',
  '/Users/evan/Desktop/node_modules',
  '/Users/evan/node_modules',
  '/Users/node_modules',
  '/node_modules' ] } }

緩存還解決了循環(huán)引用的問題耻矮。舉個例子秦躯,現(xiàn)在有模塊 a require 模塊 b;而模塊 b 又 require 了模塊 a裆装。

 // main.js
 const a = require('./a');
 console.log('in main, a.a1 = %j, a.a2 = %j', a.a1, a.a2);
 ?
 // a.js
 exports.a1 = true;
 const b = require('./b.js');
 console.log('in a, b.done = %j', b.done);
 exports.a2 = true;
 ?
 // b.js
 const a = require('./a.js');
 console.log('in b, a.a1 = %j, a.a2 = %j', a.a1, a.a2);

程序執(zhí)行結(jié)果如下:

in b, a.a1 = true, a.a2 = undefined
 in main, a.a1 = true, a.a2 = true

實際上在模塊 a 代碼執(zhí)行之前就已經(jīng)創(chuàng)建了 Module 實例寫入了緩存踱承,此時代碼還沒執(zhí)行,exports 是個空對象哨免。

 '/Users/evan/Desktop/module/a.js': 
  Module {
  exports: {},
  //...
  }
 }

代碼 exports.a1 = true; 修改了 module.exports 上的 a1 為 true, 這時候 a2 代碼還沒執(zhí)行茎活。

 '/Users/evan/Desktop/module/a.js': 
  Module {
  exports: {
  a1: true
  }
  //...
  }
 }

進入b模塊,require a.js 時發(fā)現(xiàn)緩存上已經(jīng)存在了琢唾,獲取 a 模塊上的 exports 载荔。打印 a1, a2 分別是true,和 undefined采桃。

運行完 b 模塊懒熙,繼續(xù)執(zhí)行 a 模塊剩余的代碼,exports.a2 = true; 又往 exports 對象上增加了a2屬性普办,此時 module a 的 export對象 a1, a2 均為 true工扎。

exports: { 
  a1: true,
  a2: true
 }

再回到 main 模塊,由于 require('./a.js') 得到的是 module a export 對象的引用衔蹲,這時候打印 a1, a2 就都為 true肢娘。

小結(jié)

CommonJS 模塊加載過程是同步阻塞性地加載,在模塊代碼被運行前就已經(jīng)寫入了 cache,同一個模塊被多次 require 時只會執(zhí)行一次橱健,重復(fù)的 require 得到的是相同的 exports 引用而钞。

值得留意:cache key 使用的是模塊在系統(tǒng)中的絕對位置,由于模塊調(diào)用位置的不同拘荡,相同的 require('foo')代碼并不能保證返回的是統(tǒng)一個對象引用笨忌。我之前恰巧就遇到過,兩次 require('egg-core')但是他們并不相等俱病。

ES6 模塊

ES6 模塊是前端開發(fā)同學(xué)更為熟悉的方式官疲,使用 import, export 關(guān)鍵字來進行模塊輸入輸出。ES6 不再是使用閉包和函數(shù)封裝的方式進行模塊化亮隙,而是從語法層面提供了模塊化的功能途凫。

ES6 模塊中不存在 require, module.exports, __filename 等變量,CommonJS 中也不能使用 import溢吻。兩種規(guī)范是不兼容的维费,一般來說平日里寫的 ES6 模塊代碼最終都會經(jīng)由 Babel, Typescript 等工具處理成 CommonJS 代碼。

使用 Node 原生 ES6 模塊需要將 js 文件后綴改成 mjs促王,或者 package.json "type"`` 字段改為 "module"犀盟,通過這種形式告知Node使用ES Module` 的形式加載模塊。

ES6 模塊 加載過程

ES6 模塊的加載過程分為三步:

1. 查找蝇狼,下載阅畴,解析,構(gòu)建所有模塊實例迅耘。

ES6 模塊會在程序開始前先根據(jù)模塊關(guān)系查找到所有模塊贱枣,生成一個無環(huán)關(guān)系圖,并將所有模塊實例都創(chuàng)建好颤专,這種方式天然地避免了循環(huán)引用的問題纽哥,當(dāng)然也有模塊加載緩存,重復(fù) import 同一個模塊栖秕,只會執(zhí)行一次代碼春塌。

2. 在內(nèi)存中騰出空間給即將 export 的內(nèi)容(此時尚未寫入 export value)。然后使 import 和 export 指向內(nèi)存中的這些空間簇捍,這個過程也叫連接只壳。

這一步完成的工作是 living binding import export,借助下面的例子來幫助理解垦写。

 // counter.js
 let count = 1;
 ?
 function increment () {
  count++;
 }
 ?
 module.exports = {
  count,
  increment
 }
 ?
 // main.js
 const counter = require('counter.cjs');
 ?
 counter.increment();
 console.log(counter.count); // 1

上面 CommonJS 的例子執(zhí)行結(jié)果很好理解吕世,修改 count++`` 修改的是模塊內(nèi)的基礎(chǔ)數(shù)據(jù)類型變量彰触,不會改變exports.count`梯投,所以打印結(jié)果認為 1。

 // counter.mjs
 export let count = 1;
 ?
 export function increment () {
  count++;
 }
 ?
 // main.mjs
 import { increment, count } from './counter.mjs'
 ?
 increment();
 console.log(count); // 2

從結(jié)果上看使用 ES6 模塊的寫法,當(dāng) export 的變量被修改時分蓖,會影響 import 的結(jié)果尔艇。這個功能的實現(xiàn)就是 living binding,具體規(guī)范底層如何實現(xiàn)可以暫時不管么鹤,但是知道 living binding 比網(wǎng)上文章描述為 "ES6 模塊輸出的是值的引用" 更好理解终娃。

更接近 ES6 模塊的 CommonJS 代碼可以是下面這樣:

 exports.counter = 1;
 ?
 exports.increment = function () {
  exports.counter++;
 }

3. 運行模塊代碼將變量的實際值填寫在第二步生成的空間中。

到第三步蒸甜,會基于第一步生成的無環(huán)圖進行深度優(yōu)先后遍歷填值棠耕,如果這個過程中訪問了尚未初始化完成的空間,會拋出異常柠新。

// a.mjs
 export const a1 = true;
 import * as b from './b.mjs';
 export const a2 = true;
 ?
 // b.mjs
 import { a1, a2 } from './a.mjs'
 console.log(a1, a2);

上面的例子會在運行時拋出異常 ReferenceError: Cannot access 'a1' before initialization窍荧。如果改成 import * as a from 'a.mjs'可以看到 a 模塊中 export 的對象已經(jīng)占好坑了。

 // b.mjs
 import * as a from './a.mjs'
 console.log(a);

將輸出 { a1:, a2:} 可以看出恨憎,ES6 模塊為 export 的變量預(yù)留了空間蕊退,不過尚未賦值。這里和 CommonJS 不一樣憔恳,CommonJS 到這里是知道 a1 為 true, a2 為 undefined

除此之外瓤荔,我們還能推導(dǎo)出一些 ES6 模塊和 CommonJS 的差異點:

CommonJS 可以在運行時使用變量進行 require, 例如 require(path.join('xxxx', 'xxx.js')),而靜態(tài) import 語法(還有動態(tài) import钥组,返回 Promise)不行输硝,因為 ES6 模塊會先解析所有模塊再執(zhí)行代碼。

require 會將完整的 exports 對象引入程梦,import 可以只 import 部分必要的內(nèi)容腔丧,這也是為什么使用 Tree Shaking 時必須使用 ES6 模塊 的寫法。import 另一個模塊沒有 export 的變量作烟,在代碼執(zhí)行前就會報錯愉粤,而 CommonJS 是在模塊運行時才報錯。

為什么平時開發(fā)可以混寫拿撩?

前面提到 ES6 模塊和 CommonJS 模塊有很大差異衣厘,不能直接混著寫。這和開發(fā)中表現(xiàn)是不一樣的压恒,原因是開發(fā)中寫的 ES6 模塊最終都會被打包工具處理成 CommonJS 模塊影暴,以便兼容更多環(huán)境,同時也能和當(dāng)前社區(qū)普通的 CommonJS 模塊融合探赫。

在轉(zhuǎn)換的過程中會產(chǎn)生一些困惑型宙,比如說:

__esModule 是什么?干嘛用的伦吠?

使用轉(zhuǎn)換工具處理 ES6 模塊的時候妆兑,郴昀梗看到打包之后出現(xiàn) __esModule 屬性,字面意思就是將其標(biāo)記為 ES6 Module搁嗓。這個變量存在的作用是為了方便在引用模塊的時候加以處理芯勘。

例如 ES6 模塊中的 export default 在轉(zhuǎn)化成 CommonJS 時會被掛載到 exports['default'] 上,當(dāng)運行 require('./a.js') 時 是不能直接讀取到 default 上的值的腺逛,為了和 ES6 中 import a from './a.js'的行為一致荷愕,會基于 __esModule 判斷處理。

 // a.js
 export default 1;
 ?
 // main.js
 import a from './a';
 ?
 console.log(a);

轉(zhuǎn)化后

// a.js
 Object.defineProperty(exports, "__esModule", {
  value: true
 });
 exports.default = 1;
 ?
 // main.js
 'use strict';
 ?
 var _a = require('./a');
 ?
 var _a2 = _interopRequireDefault(_a);
 ?
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 ?
 console.log(_a2.default);

a 模塊 export defualt 會被轉(zhuǎn)換成 exports.default = 1;棍矛,這也是平時前端項目開發(fā)中使用 require 為什么還常常需要 .default 才能取到目標(biāo)值的原因安疗。

接著當(dāng)運行 import a from './a.js' 時,es module 預(yù)期的是返回 export 的內(nèi)容够委。工具會將代碼轉(zhuǎn)換為 _interopRequireDefault 包裹茂契,在里面判斷是否為 esModule,是的話直接返回慨绳,如果是 commonjs 模塊的話則包裹一層 {default: obj}掉冶,最后獲取 a 的值時,也會被裝換成 _a1.default脐雪。

總結(jié)

大家有什么要說的厌小,歡迎在評論區(qū)留言

對了,小編為大家準備了一套2020最新的web前端資料战秋,需要點擊下方鏈接獲取方式

1璧亚、點贊+評論(勾選“同時轉(zhuǎn)發(fā)”)

學(xué)習(xí)前端,你掌握這些脂信。二線也能輕松拿8K以上

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末癣蟋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狰闪,更是在濱河造成了極大的恐慌疯搅,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埋泵,死亡現(xiàn)場離奇詭異幔欧,居然都是意外死亡,警方通過查閱死者的電腦和手機丽声,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門礁蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雁社,你說我怎么就攤上這事浴井。” “怎么了霉撵?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵磺浙,是天一觀的道長洪囤。 經(jīng)常有香客問我,道長屠缭,這世上最難降的妖魔是什么箍鼓? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任崭参,我火速辦了婚禮呵曹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘何暮。我一直安慰自己奄喂,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布海洼。 她就那樣靜靜地躺著跨新,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坏逢。 梳的紋絲不亂的頭發(fā)上域帐,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音是整,去河邊找鬼肖揣。 笑死,一個胖子當(dāng)著我的面吹牛浮入,可吹牛的內(nèi)容都是我干的龙优。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼事秀,長吁一口氣:“原來是場噩夢啊……” “哼彤断!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起易迹,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤宰衙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后睹欲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菩浙,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年句伶,在試婚紗的時候發(fā)現(xiàn)自己被綠了劲蜻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡考余,死狀恐怖先嬉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楚堤,我是刑警寧澤疫蔓,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布含懊,位于F島的核電站,受9級特大地震影響衅胀,放射性物質(zhì)發(fā)生泄漏岔乔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一滚躯、第九天 我趴在偏房一處隱蔽的房頂上張望雏门。 院中可真熱鬧,春花似錦掸掏、人聲如沸茁影。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽募闲。三九已至,卻和暖如春愿待,著一層夾襖步出監(jiān)牢的瞬間浩螺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工仍侥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留要出,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓访圃,卻偏偏與公主長得像厨幻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子腿时,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345