摘要:以后需要引用模塊的變量函數類就在這個模塊對象的取出,即使再次進來模塊也不會重新執行,只會從緩存獲取。所以對相同模塊的再次加載都是優先緩存方式,核心模塊的緩存檢查依然優先于文件模塊。內建模塊導出啟動會生成全局變量,提供方法協助加載內建模塊。
原始時代
作為一門語言的引入代碼方式,相較于其他如PHP的include和require,Ruby的require,Python的import機制,Javascript是直接使用
出于需要社區制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于服務器,后者用于瀏覽器。
CommonJS規范CommonJS規范為Javascript制定的美好愿景是希望Javascript能夠在任何地方運行,具備跨宿主環境執行的能力,例如:
富客戶端應用
服務器端Javascript應用程序(如Nodejs )
命令行工具
桌面圖形界面應用程序
混合應用(Titanium和Adobe AIR等形式應用)
這些規范基本覆蓋了模塊,二進制,Buffer,字符集編碼,I/O流,進程環境,文件系統,套接字,單元測試,Web服務器網關接口,包管理等。
模塊規范1, 引用
模塊上下文提供 require() 方法引入外部模塊,一般如下
var fs = require("fs");
2, 定義
模塊中存在一個上下文 module對象 ,它代表模塊自身, module對象 提供了 exports對象 用于導出當前模塊的變量、函數、類,并且是唯一的導出出口。
//a.js模塊 exports.a = 1; //引用a.js模塊 var a = require("a");
3, 標識
require() 方法接受小駝峰命名的字符串,或者相對/絕對路徑,并且可以省略文件后綴,它有自己一套匹配規則,后面再講。
"./" 開頭表示相對路徑引用模塊;
"/" 開頭表示絕對路徑引用模塊;
不帶上面符號開頭的小駝峰字符串表示默認提供的核心模塊或者 node_modules 下安裝模塊;
不帶上面符號開頭的路徑字符串表示 node_modules 下安裝模塊對應后續路徑;
//a.js模塊 var a = require("./a"); var a = require("/a"); var a = require("a"); var a = require("a/a");
至此看來使用相當簡單,模塊的意義在于將類聚的變量、函數、類等限定在私有作用域中,同時支持引入導出功能連接上下游依賴,避免了變量污染等問題。
module.exports 和exports的關系?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 estmodule_demo est1.js", // loaded: false, // children: [], // paths: // [ "C:project estmodule_demo ode_modules", // "C:project est ode_modules", // "C:project ode_modules", // "C: ode_modules" ] }
以后需要引用模塊的變量、函數、類就在這個模塊對象的 exports 取出,即使再次 require() 進來模塊也不會重新執行,只會從緩存獲取。
CommonJS優點模塊引用順序決定加載順序;
每個模塊只會加載一次,然后將運行結果緩存起來二次利用,以后再次加載就直接讀取緩存。要想讓模塊再次運行,必須清除緩存;
每個模塊都有其多帶帶的作用域,不會污染全局;
Nodejs 模塊實現Nodejs 借鋻了 CommonJS 但不完全按照規范實現了自己的模塊系統。
在Nodejs 引入模塊會經歷三個步驟:
路徑分析;
文件定位;
編譯執行;
在 Nodejs 中有兩種模塊
Nodejs 提供的核心模塊;
這部分模塊在 Nodejs 源代碼編譯過程中編譯進了二進制執行文件。在 Nodejs 進程啟動時部分核心模塊被直接加載進了內存中,所以在引用的時候可以省去文件定位和編譯執行的步驟,并且在路徑分析優先判斷,所以加載速度是最快的。
由用戶編寫的文件模塊;
這部分模塊在運行時動態加載,需要經歷完整步驟。
Nodejs 會對引用過的模塊進行緩存以減少二次引入的開銷。而且緩存的是模塊編譯和執行之后的對象。所以 require() 對相同模塊的再次加載都是優先緩存方式,核心模塊的緩存檢查依然優先于文件模塊。
Nodejs 模塊標識前面提過的模塊標識,例如:
核心模塊fs等
優先級僅次于緩存加載,如果直接引用自己編寫的和核心模塊具有相同標識的模塊會引用失敗,必須選擇不同標識符或者使用路徑方式加載。
路徑形式文件模塊
分析過程中 rerquire() 會將路徑轉換成真實路徑,并以此為索引將編譯后結果緩存起來,因為指明了模塊位置所以查找過程會省點時間,速度慢于核心模塊。
自定義模塊
可能是以包或者文件形式的特殊模塊,查找費時速度最慢的一種,因為他會用到模塊路徑的查找方法。
Nodejs在定位文件模塊有自己的一套查找策略,你可以隨便一個文件夾執行一個腳本如下看看打印信息,我是 Windows 系統結果如下
console.log(module.paths); // [ "C:workproject est ode_modules", // "C:workproject ode_modules", // "C:work ode_modules", // "C: ode_modules" ]
從中可以看出他會從當前執行文件所在目錄下的 node_modules,沿路徑向上逐層遞歸查找 node_modules 直到根目錄為止。
模塊加載過程會逐個嘗試直到符合條件或者沒有符合為止,你可以看出里面有著很明顯的問題。
層級越深查找起來越費時費力;
可能你衹想查看當前目錄,但是它失敗后會自動嘗試其他路徑;
這就是自定義模塊最慢的原因。
文件定位擴展名分析
Nodejs 在標識符不包含后綴情況下會以.js, .json, .node的次序逐個嘗試匹配,而且過程中需要利用fs模塊以同步阻塞方式去判斷是否匹配,所以在非.js文件情況指明后綴能減少性能損耗的問題。
目錄分析和包
還有一種情況是經過上面步驟之后都匹配不到對應文件但是有符合的目錄,此時Nodejs 會將其作為一個包的方式處理。
1)查找包下的 package.json 文件(包描述文件),通過 JSON.parse() 解析出文件讀取里面的 main 屬性定位對應的文件,省略后綴情況下需要執行擴展名分析步驟。
2)如果沒有 package.json 或者 main 屬性不對,會用默認值 index 去查找匹配文件,這一步需要擴展名分析步驟逐個嘗試。
3)如果還是失敗就會根據模塊路徑規則往上層路徑尋找,直到全部路徑都沒有匹配文件就拋出失敗。
這是引入模塊的最后階段,定位到目標文件之后會新建一個模塊對象,然后根據路徑載入進行編譯,不同后綴文件載入方式不同:
js通過fs模塊同步讀取文件之后編譯執行;
node是C/C++編寫的擴展文件,通過 dlopen()方法 加載最后編譯生成的對象;
json通過fs模塊同步讀取文件之后用 JSON.parse() 解析返回結果;
其余默認js處理方式;
每個編譯成功之后的模塊都會以其文件路徑作為索引緩存在 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目錄。
Javascript核心模塊編譯 轉存為C/C++代碼Nodejs 采用V8附帶的 js2c.py工具 將內置的Javascript代碼(src/node.js和lib/*.js)轉成C++的數組,生成 node_natives.h 頭文件,Javascript代碼以字符串形式存儲在nodejs命名空間里,此時還不能直接執行。等 Nodejs 啟動進程時候才被直接加載進內存中,所以不需要引入就能直接使用。
編譯Javascript核心模塊和文件模塊一樣也會被包裝成模塊對象,區別在于獲取源代碼的方式以及緩存執行結果的位置。
核心模塊源文件通過 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 填充空對象,最后按模塊名緩存起來并返回給調用方使用。
至此我們已經有個大概概念了,梳理一下各種模塊之間的關系:
C/C++內建模塊是最底層核心模塊,主要提供API給Javascript核心模塊和第三方Javascript模塊使用;
Javascript核心模塊分兩類,一類作為C/C++內建模塊的封裝層和橋接層,一類純粹的功能模塊;
文件模塊分Javascript模塊和C/C++擴展模塊;
ES6模塊加載直到ES6標準化模塊功能,統一替代了之前多種模塊實現庫,成為瀏覽器和服務器通用的模塊解決方案。ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量、函數、類。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西。
ES6 的模塊有幾個需要注意的地方:
自動采用嚴格模式,即使你沒有使用"use strict";
頂層的this指向undefined;
// CommonJS模塊 let {readFile} = require("fs"); // ES6模塊 import {readFile} from "fs";
以上為例。
CommonJS加載整個 fs模塊 生成一個模塊對象,然后從對象中導出 readFile方法 。
ES6 模塊通過 import命令 從 fs模塊 加載輸入的變量、函數、類。
結果就是ES6模塊效率高,但是拿不到模塊對象本身。
加載方案 | 加載 | 輸出 |
---|---|---|
CommonJS | 運行時加載 | 拷貝 |
ES6 模塊 | 編譯時輸出接口 | 引用 |
由于 ES6 模塊是編譯時加載,使得靜態分析成為可能。比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。
ES6 模塊還有以下好處:
不再需要UMD模塊格式了,將來服務器和瀏覽器都會支持 ES6 模塊格式。目前,通過各種工具庫,其實已經做到了這一點。
將來瀏覽器的新 API 就能用模塊格式提供,不再必須做成全局變量或者 navigator對象 的屬性。
不再需要對象作為命名空間(比如Math對象),未來這些功能可以通過模塊提供。
ES6模塊提供了 export導出命令 和 import導入命令 ,它們同樣具有全局提升的效果,只要在頂層使用即可。
export導出命令支持輸出變量、函數、類。
//變量 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會得到2import 導入命令
和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";關鍵字default
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;export 與 import 的復合寫法
這里提供了兩種寫法,他們之間會有些不同。
//引入后導出 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";Nodejs 使用問題
import命令 會被 JavaScript引擎靜態分析,先于模塊內的其他語句執行,而 Nodejs 的 require() 是運行時加載模塊,import命令無法取代require的動態加載功能,所以如果在Nodejs 中使用ES6模塊語法要注意這一點。
//成功 var fs = require("f"+"s"); //報錯 import fs from ("f"+"s");
有一個提案,建議引入 import() 函數,完成動態加載,已經有實現方案了,我沒用過就不說了。
參考資料nodejs 深入淺出
ES6 標準入門(第3版)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106521.html
摘要:要求模塊編寫必須在真正的代碼之外套上一層規定的代碼包裝,樣子看起來是這樣的模塊代碼通過傳遞一個簽名為的回調函數給函數,就可以把需要注入的變量和函數注入到模塊代碼內。 之前寫的文章急速Js全棧教程得到了不錯的閱讀量,霸屏掘金頭條3天,點贊過千,閱讀近萬,甚至還有人在評論區打廣告,可見也是一個小小的生態了;)。看來和JS全棧有關的內容,還是有人頗有興趣的。 showImg(https://...
摘要:提倡依賴前置,在定義模塊的時候就要聲明其依賴的模塊。適用場景按需加載條件加載動態的模塊路徑注關于模塊化,詳細見阮一峰的入門模塊與模塊化區別模塊化的規范和兩種。 模塊化開發方便代碼的管理,提高代碼復用性,降低代碼耦合,每個模塊都會有自己的作用域。當前流行的模塊化規范有CommonJS,AMD,CMD,ES6的import/export CommonJS的主要實踐者就是nodejs,一般...
摘要:模塊的加載第一個參數,是一個數組,里面的成員就是要加載的模塊第二個參數,則是加載成功之后的回調函數。異步加載,瀏覽器不會失去響應它指定的回調函數,只有前面的模塊都加載成功后,才會運行,解決了依賴性的問題。 什么是模塊化? 模塊化就是把系統分離成獨立功能的方法,這樣我們需要什么功能,就加載什么功能。 優點:可維護性:根據定義,每個模塊都是獨立的,良好設計的模塊會盡量與外部的代碼撇清關系,...
閱讀 628·2021-09-24 09:48
閱讀 2491·2021-08-26 14:14
閱讀 517·2019-08-30 13:08
閱讀 1445·2019-08-29 15:22
閱讀 3066·2019-08-29 11:06
閱讀 1001·2019-08-26 18:26
閱讀 1033·2019-08-26 13:53
閱讀 2511·2019-08-26 12:21