做爲一門語言的引入代碼方式,相較於其餘如PHP的include和require,Ruby的require,Python的import機制,Javascript是直接使用 <script> 標籤。
由於Javascript是一門單線程語言,GUI渲染線程和Javascript引擎線程是互斥的,代碼執行 <script> 標籤GUI渲染線程會掛起,而後下載資源,執行腳本,完成以後再繼續往下執行。在那段時間內界面是不會響應用戶操做的。用戶體驗至關不友好。同時還帶來一系列的隱患:javascript
<script>
標籤也提供了 defer 和 async 屬性能夠實現異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令,區別在於:html
<script src="xx.js" defer></script> <script src="xx.js" async></script>
出於須要社區制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用於服務器,後者用於瀏覽器。java
CommonJS規範爲Javascript制定的美好願景是但願Javascript可以在任何地方運行,具有跨宿主環境執行的能力,例如:node
這些規範基本覆蓋了模塊,二進制,Buffer,字符集編碼,I/O流,進程環境,文件系統,套接字,單元測試,Web服務器網關接口,包管理等。git
1, 引用
模塊上下文提供 require() 方法引入外部模塊,通常以下es6
var fs = require('fs');
2, 定義
模塊中存在一個上下文 module對象 ,它表明模塊自身, module對象 提供了 exports對象 用於導出當前模塊的變量、函數、類,而且是惟一的導出出口。json
//a.js模塊 exports.a = 1; //引用a.js模塊 var a = require('a');
3, 標識
require() 方法接受小駝峯命名的字符串,或者相對/絕對路徑,而且能夠省略文件後綴,它有本身一套匹配規則,後面再講。數組
//a.js模塊 var a = require('./a'); var a = require('/a'); var a = require('a'); var a = require('a/a');
至此看來使用至關簡單,模塊的意義在於將類聚的變量、函數、類等限定在私有做用域中,同時支持引入導出功能鏈接上下游依賴,避免了變量污染等問題。瀏覽器
exports 是引用 module.exports 的值,而真正導出的是 module.exports ,接着就是基本類型和引用類型的區別。
若是直接替換 module.exports 或者exports至關於切斷了和原有對象之間的關聯,後續二者互不影響了。緩存
第一次 require() 一個腳本的時候會執行代碼而後在內存中會生成一個模塊對象緩存起來,相似
{ id: '...',//模塊的識別符,一般是帶有絕對路徑的模塊文件名 filename: '',//模塊的文件名,帶有絕對路徑 exports: {...},//導出變量、函數、類 loaded: true,//模塊是否已經完成加載 parent: {},//調用該模塊的模塊 children: [],//該模塊要用到的其餘模塊 ... }
例如你建立一個文件腳本代碼執行就能夠查看到這些信息。
exports.a = 1; console.log(module); // Module { // id: '.', // exports: { a: 1 }, // parent: null, // // filename: 'C:\\project\\test\\module_demo\\test1.js', // loaded: false, // children: [], // paths: // [ 'C:\\project\\test\\module_demo\\node_modules', // 'C:\\project\\test\\node_modules', // 'C:\\project\\node_modules', // 'C:\\node_modules' ] }
之後須要引用模塊的變量、函數、類就在這個模塊對象的 exports 取出,即便再次 require() 進來模塊也不會從新執行,只會從緩存獲取。
Nodejs 借鋻了 CommonJS 但不徹底按照規範實現了本身的模塊系統。
在Nodejs 引入模塊會經歷三個步驟:
在 Nodejs 中有兩種模塊
Nodejs 會對引用過的模塊進行緩存以減小二次引入的開銷。並且緩存的是模塊編譯和執行以後的對象。因此 require() 對相同模塊的再次加載都是優先緩存方式,核心模塊的緩存檢查依然優先於文件模塊。
前面提過的模塊標識,例如:
Nodejs在定位文件模塊有本身的一套查找策略,你能夠隨便一個文件夾執行一個腳本以下看看打印信息,我是 Windows 系統結果以下
console.log(module.paths); // [ 'C:\\work\\project\\test\\node_modules', // 'C:\\work\\project\\node_modules', // 'C:\\work\\node_modules', // 'C:\\node_modules' ]
從中能夠看出他會從當前執行文件所在目錄下的 node_modules,沿路徑向上逐層遞歸查找 node_modules 直到根目錄爲止。
模塊加載過程會逐個嘗試直到符合條件或者沒有符合爲止,你能夠看出裏面有着很明顯的問題。
這就是自定義模塊最慢的緣由。
這是引入模塊的最後階段,定位到目標文件以後會新建一個模塊對象,而後根據路徑載入進行編譯,不一樣後綴文件載入方式不一樣:
每一個編譯成功以後的模塊都會以其文件路徑做爲索引緩存在 Module_cache。根據不一樣的擴展後綴 Nodejs 有不一樣的讀取方式。
1, Javascript模塊編譯
在編譯過程當中,Nodejs 會對獲取的模塊進行包裝,以下:
(function(exports, require, module, __filename, __dirname) { //模塊源碼 })
2, C/C++模塊編譯
Nodejs 調用 process.dlopen() 方法進行加載執行,經過 libuv封裝庫 支持 Windows 和 *nix 平臺下實現,由於.node自己就是C/C++寫的,因此它不須要編譯,衹要加載執行就能夠了,執行效率較高。
3, JSON文件編譯
上面說過經過fs模塊同步讀取文件以後用 JSON.parse() 解析返回結果,賦值給模塊對象的 exports。
除了配置文件,若是你開發中有須要用到json文件的時候能夠不用 fs模塊 去讀取,而是直接 require() 引入更好,由於能享受到緩存加載的便利。
上面說過 Nodejs 模塊分爲核心模塊和文件模塊,剛纔講的都是文件模塊的編譯過程,而 Nodejs 的核心模塊在編譯成可執行文件過程當中會被編譯進二進制文件。核心模塊也分Javascript和C/C++編寫,前者在Node的lib目錄,後者在Node的src目錄。
Nodejs 採用V8附帶的 js2c.py工具 將內置的Javascript代碼(src/node.js和lib/*.js)轉成C++的數組,生成 node_natives.h 頭文件,Javascript代碼以字符串形式存儲在nodejs命名空間裏,此時還不能直接執行。等 Nodejs 啓動進程時候才被直接加載進內存中,因此不須要引入就能直接使用。
和文件模塊同樣也會被包裝成模塊對象,區別在於獲取源代碼的方式以及緩存執行結果的位置。
核心模塊源文件經過 process.binding('natives') 取出,編譯完成後緩存到 NativeModule._cache 對象上,而文件模塊會被緩存到 Module._cache。
每一個內建模塊在定義以後會經過 NODE_MODULE宏 將模塊定義到nodejs命名空間,模塊的具體初始化方法被掛載在結構的 register_func 成員。
node_extensions.h 文件將散列的內建模塊統一放進 node_module_list數組 中,Nodejs 提供了 get_builtin_module() 方法從中取出。
內建模塊優點在於自己C/C++編寫性能優異,編譯成二進制文件時候被直接加載進內存,無需再作標識符定位,文件定位,編譯等過程。
Nodejs 啓動會生成全局變量 process,提供 Binding() 方法協助加載內建模塊。
加載過程當中咱們會先生成 exports空對象 ,而後調用 get_builtin_module() 方法去取內建模塊,經過執行 register_func 填充空對象,最後按模塊名緩存起來並返回給調用方使用。
至此咱們已經有個大概概念了,梳理一下各類模塊之間的關係:
直到ES6標準化模塊功能,統一替代了以前多種模塊實現庫,成爲瀏覽器和服務器通用的模塊解決方案。ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量、函數、類。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。
ES6 的模塊有幾個須要注意的地方:
// CommonJS模塊 let {readFile} = require('fs'); // ES6模塊 import {readFile} from 'fs';
以上爲例。
CommonJS加載整個 fs模塊 生成一個模塊對象,而後從對象中導出 readFile方法 。
ES6 模塊經過 import命令 從 fs模塊 加載輸入的變量、函數、類。
結果就是ES6模塊效率高,可是拿不到模塊對象自己。
加載方案 | 加載 | 輸出 |
---|---|---|
CommonJS | 運行時加載 | 拷貝 |
ES6 模塊 | 編譯時輸出接口 | 引用 |
因爲 ES6 模塊是編譯時加載,使得靜態分析成爲可能。好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
ES6 模塊還有如下好處:
支持輸出變量、函數、類。
//變量 export var a = 1; //函數 export function log(n) { console.log(n); } //類 export class Num {}
我習慣寫法是使用對象方式輸出,整個模塊導出什麼一目瞭然。
//變量 var a = 1; //函數 function log(n) { console.log(n); } //類 class Num {} export {a, log, Num};
這種寫法也支持as關鍵字對外重命名
export {a as b, log as cng, Num as Digit};
這裏有一個隱藏比較深的概念性知識,export命令規定的是對外的接口必須與模塊內部的變量、函數、類創建一一對應關係。這種寫法是OK的。
export var a = 1; //或者 var a = 1; export { a, //或者 a as b, }
可是你不能這麼寫,儘管看起來沒什麼問題,不過沒有提供對外的接口,只是直接或者間接輸出1。
export 1; //或者 var a = 1; export a
特別容易讓人混淆的是這一句,因此要特別注意
//正確 export var a = 1; //錯誤 var a = 1; export a
這不只僅是針對變量,包括函數和類也遵循這種寫法,之因此會有這種要求是由於 export語句 輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。
export var a = 1 setTimeout(() => a = 2, 3000); //後續引用a會獲得2
和expor相對應的按需引入寫法以下
//直接引入寫法 import {a, log, Num} from 'xx';
import也支持使用as關鍵字
import {a as b, log as cng, Num as Digit} from 'xx';
和 export 動態綁定值不一樣,import 是隻讀靜態執行,即你不能修改引用的模塊變量、函數、類等,也不能使用表達式和變量這種運行時才能引入靜態分析階段無法獲得值的寫法。
//修改屬性 import{a} from 'xx' a = 2//error
//表達式引入 import{'l' + 'og'} from 'xx' //變量引入 var module = 'xx'; import {} from module//error
//判斷引入 //error if (true) { import {} from 'xx1'; } else { import {} from 'xx2'; }
由於屢次引用也只會執行一次,儘管不推薦,可是這種寫法也是能夠的
import {a} from 'xx'; import {log} from 'xx'; //等價於 import {a, log} from 'xx';
import也支持這種寫法,僅僅執行模塊,可是不輸入任何變量、函數、類。
import 'xx';
export 支持 關鍵字default 設置默認導出的變量、函數、類:
1, 每一個模塊只支持一個關鍵字default默認導出;
2, 可使用函數名或匿名函數導出,即便指定了函數名也不能在模塊外部引用,等同視爲匿名函數加載;
//函數 function log(n) { console.log(n); } export default log; //或者 export default function(n) { console.log(n); } //或者 export default function log(n) { console.log(n); }
其餘模塊加載該模塊時,import命令能夠爲該默認導出函數指定任意名字。
export default function log(n) { console.log(n); } //加載 import anyName from 'xx';
若是想在一條 import語句 中,同時輸入默認函數和其餘接口,能夠寫成下面這樣。
import log, {a, Num as Digit} from 'xx';
本質上這也只是一種語法糖,與下面寫法等價
export default log; import log from 'xx'; //==等價== export { log as default} import { default as log } from 'xx';
由於default也是變量,因此不能後面再加變量
export default var a = 1;
可是能夠直接輸出
export default 1; export default a;
用星號(*)指定一個對象,全部輸出值都加載在這個對象上面。
import * as all from 'xx'; const {a, log, Num} = all;
這裏提供了兩種寫法,他們之間會有些不一樣。
//引入後導出 import {log} from 'xx'; export {log}; //直接導出 export {log} from 'xx'; //或者 export {log as default} from 'xx';
區別在於第二三種是沒有導入動做,因此不能在該模塊引用對應的變量、函數、類。
須要注意的是下面三種寫法ES6目前還不支持。
export * as all from "xx"; export all from "xx"; export log, {a, Digit as Num} from 'xx';
import命令 會被 JavaScript引擎靜態分析,先於模塊內的其餘語句執行,而 Nodejs 的 require() 是運行時加載模塊,import命令沒法取代require的動態加載功能,因此若是在Nodejs 中使用ES6模塊語法要注意這一點。
//成功 var fs = require('f'+'s'); //報錯 import fs from ('f'+'s');
有一個提案,建議引入 import() 函數,完成動態加載,已經有實現方案了,我沒用過就不說了。