AMD、CMD、CommonJS、ES6
一、AMD
AMD,Asynchronous Module Definition, 非同步模組定義
。它是一個在瀏覽器端模組化開發的規範。
它不是javascript
原生支援,所以使用AMD規範進行頁面開發需要用到對應的庫,也就是RequireJS
,AMD其實是RequireJS
在推廣的過程中對模組定義的範圍化的產出。
requireJS
主要解決兩個問題:
- 多個
js
檔案存在依賴關係時,被依賴的檔案需要早於依賴它的檔案載入到瀏覽器 js
載入的時候瀏覽器會阻塞渲染執行緒,載入檔案越多,頁面失去響應的時間越長
用法:
require
需要一個root
來作為搜尋依賴的開始(類似package.json
的main
),data-main
來指定這個root
。
<script src="script/require.js" data-main="script/app.js"></script>
複製程式碼
這樣就指定了root
是app.js
,只有直接或者間接與app.js
有依賴關係的module
才會被插入到html
中。
define()
函式:用來定義模組的函式。args0
: 需引入模組的名字陣列,arg1
:依賴引入之後的callback
,callback
的引數就是引入的東西。如果有多個依賴,則引數按照引入的順序依次傳入。
define(['dependence_name'], (args) => {
// args就是從dependence_name引入的東西
// ... Your fucking code ...
return your_export;
});
複製程式碼
require()
函式: 用來引入模組的函式。
require(['import_module_name'], (args) => {
// args就是從import_module_name引入的東西
// ... Your fucking code ...
});
複製程式碼
require.config
配置:baseUrl
:載入module
的根路徑paths
:用於對映不存在根路徑下面的模組路徑shimes
:載入非AMD
規範的js
二、CMD
CMD, Common Module Definition
, 通用模組定義。
CMD
是在sea.js
推廣的過程中產生的。在CMD
規範中,一個模組就是一個檔案。
define(function(require, exprots, module) {
const fs = require('fs'); //接受模組標識作為唯一引數
// exports,module則和CommonJS類似
exports.module = {
props: 'value'
};
});
seajs.use(['test.js'], function(test_exports) {
// ....
});
複製程式碼
null | AMD | CMD |
---|---|---|
定義module時對依賴的處理 | 推崇依賴前置,在定義的時候就要宣告其依賴的模組 | 推崇就近依賴,只有在用到這個module的時候才去require |
載入方式 | async | async |
執行module的方式 | 載入module完成後就會執行該module,所有module都載入執行完成後會進入require的回撥函式,執行主邏輯。依賴的執行順序和書寫的順序不一定一致,誰先下載完誰先執行,但是主邏輯 一定在所有的依賴載入完成後才執行(有點類似Promise.all)。 | 載入完某個依賴後並不執行,只是下載而已。在所有的module載入完成後進入主邏輯,遇到require語句的時候才會執行對應的module。module的執行順序和書寫的順序是完全一致的。 |
三、CommonJS
English time: Common -- 常識 W3C官方定義的API都只能基於Browser,而CommonJS則彌補了javascript這方面的不足。
NodeJS
是CommonJS
規範的主要實踐者。它有四個重要的環境變數為模組化的實現提供支援:module、exports、require、global
。
實際用時,使用module.exports
(不推薦使用exports)定義對外輸出的API,用require
來引用模組。CommonJS
用同步的方式載入模組。在Server
上模組檔案都在本地磁碟,所以讀取非常快沒什麼不妥,但是在Browser
由於網路的原因,更合理的方案是非同步載入。
CommonJS
對模組的定義主要分為:模組引用、模組定義、模組標識3個部分。
1、模組引用:
const fs = require('fs');
複製程式碼
require的執行步驟:
- 如果是核心模組, 如fs,則直接返回模組
- 如果是路徑,則拼接成一個絕對路徑,然後先讀取快取require.cache再讀取檔案。(如果沒有副檔名,則以
js => json => node
(以二進位制外掛模組的方式去讀取)的順序去識別) - 首次載入後的模組會在
require.cache
中,所以多次require,得到的物件是同一個(引用的同一個物件) - 在執行模組程式碼的時候,會將模組包裝成以下模式,以便於作用域在模組範圍之內。
(function (exports, require, module, __filename, __dirname) {
// module codes
});
複製程式碼
- 包裝之後的程式碼同過vm原生模組的runInThisContext()方法執行(類似eval,不過具有明確上下文不會汙染環境),返回一個function物件。
最後將當前模組物件的
exports
、require
方法、module
以及檔案定位中得到的完整檔案路徑
(包括檔名)和檔案目錄
傳遞給這個function執行。
2、模組定義:
function fn() {}
exports.propName = fn;
module.exports = fn;
複製程式碼
一個module
物件代表模組本身,exports
是module
的屬性。一般通過在exports
上掛載屬性即可定義匯出,也可以直接給module.exports
賦值來定義匯出(推薦)。
3、模組標識:
模組標識就是傳遞給require()
方法的引數,可以是相對路徑或者絕對路徑,也可以是符合小駝峰命名的字串。
NodeJS
中CommonJS
的實現:Node
中模組分為Node提供的核心模組
和使用者編寫的檔案模組
。
核心模組在Node
原始碼的編譯過程中,編譯進了二進位制執行檔案。在Node
啟動的時候部分核心模組就載入到了memory
中,所以在引用核心模組的時候,檔案定位和編譯執行步驟可以省略,並且在路徑判斷中優先判斷,所以它的載入速度是最快的。
檔案模組則是在執行時動態載入,需要完整的路徑分析,檔案定位、編譯執行等過程,速度較核心模組慢。
在NodeJS
中引入模組需要經歷如下3個步驟:
-
路徑分析:module.paths = [‘當前目錄下的node_modules’, ‘父目錄下的node_modules’, …, ‘跟目錄下的node_modules’]
-
檔案定位:副檔名分析、目錄和包的處理。
- 副檔名分析:
Node
會按.js => .json => .node
的次序補足副檔名依次嘗試。(在嘗試的過程中會呼叫同步的fs模組來檢視檔案是否存在) - 目錄和包的處理:可能沒有對應的檔案,但是存在相應的目錄。這時
Node
會在此目錄中查詢package.json
,並JSON.parse
出main
(入口檔案)對應的檔案。如果main
屬性錯誤或者沒有package.json
,則將index
作為main
。如果沒有定位成功任何檔案,則到下一個模組路徑重複上述工作,如果整個module.paths
都遍歷完都沒有找到目標檔案,則跑出查詢失敗錯誤。
- 副檔名分析:
-
編譯執行:在
Node
中每個模組檔案都是一個物件,編譯執行是引入檔案模組的最後一個階段。定位到檔案後,Node
會新建一個模組物件,然後根據路徑載入並編譯。對於不同的副檔名,其載入的方式也有所不同:.js
: 通過fs
模組同步讀取檔案後編譯執行.node
:這是C++
編寫的擴充套件檔案,通過dlopen()
載入最後編譯生成的檔案。.json
:同.js
檔案,之後用JSON.parse
解析返回結果。 其餘檔案: 都按js
的方式解析。
null | CommonJS | ES6 |
---|---|---|
keywords | exports, require, module, __filename. __dirname | import, export |
匯入 | const path = require('fs'); 必須將一個模組匯出的所有屬性都引入 | import path from 'path'; 可以只引入某個 |
匯出 | module.exports = App; | export default App; |
匯入的物件 | 隨意修改 值的copy | 不能隨意修改 值的reference |
匯入次數 | 可以任意次require,除了第一次,之後的require都是從require.cache中取得 | 在頭部匯入,只能匯入一次 |
載入 | 執行時載入 | 編譯時輸出介面 |
ES6模組
ES6的模組已經比較熟悉了,用法不多贅述,直接上碼:
import { prop } from 'app'; //從app中匯入prop
import { prop as newProp } from 'app'; // 功能和上面一樣,不過是將匯入的prop重新命名為newProp
import App from 'App'; // 匯入App的default
import * as App from 'App'; // 匯入App的所有屬性到App物件中
export const variable = 'value'; // 匯出一個名為variable的常量
export {variable as newVar}; // 和import 的重新命名類似,將variable作為newVar匯出
export default variable = 'value'; // 將variable作為預設匯出
export {variable as default}; // 和上面的寫法基本一樣
export {variable} from 'module'; // 匯出module的variable ,該模組中無法訪問
export {variable as newVar} from 'module'; // 下面的自己看 不解釋了
export {variable as newVar} from 'module';
export * from 'module';
複製程式碼
ps:ES6模組匯入的變數(其實應該叫常量更準確)具有以下特點: 變數提升、相當於被
Object.freeze()
包裝過一樣、import/export只能在頂級作用域
ES6
模組區別於CommonJS
的執行時載入,import
命令會被JavaScript
引擎靜態分析,優先於模組內的其他內容執行(類似於函式宣告優先於其他語句那樣), 也就是說在檔案的任何位置import
引入模組都會被提前到檔案頂部。
ES6
的模組 自動開啟嚴格模式,即使沒有寫'use strict';
。
執行一個包含import
宣告的模組時,被引入的模組先匯入並載入,然後根據依賴關係,每個模組的內容會使用深度優先的原則進行遍歷。跳過已經執行過的模組,避免依賴迴圈。
okey~接下來老哥再看看(查查)import
到底幹啥了:
標準幾乎沒有談到import
該做什麼,ES6
將模組的載入細節完全交給了實現。
大致來說,js
引擎執行一個模組的時候,其行為大致可歸納為以下四步:
- 解析:engine去解析模組的程式碼,檢查語法等。
- 載入:遞迴載入所有被引入的模組,深度優先。
- 連結:為每個新載入的模組建立一個作用域,並將模組中的宣告綁入其中(包括從其他模組中引入的)。
當
js
引擎開始執行載入進來的模組中的程式碼的時候,import
的處理過程已經完了,所以js
引擎執行到一行import
宣告的時候什麼也不會幹。引入都是靜態實現的,等到程式碼執行的時候就啥都不幹了。
既然說到了模組(module),那就順便提一下它和指令碼(script)的區別(注意,我這裡說的區別僅限於在Web瀏覽器中):
- | module | script |
---|---|---|
使用方式 (當然還有其他的執行方式,在這裡不做過多討論) | <script src="./source.js type="module" /> | <script src="./source.js type="text/javascript" /> |
下載 | ①遇到<script>時,會自動應用defer。 ②下載 && 解析module。 ③遞迴下載module中匯入的資源。下載階段完成。 |
遇到<script>時預設阻塞文件渲染,開啟下載。 |
執行方式 | ①下載完成後會遞迴執行module中匯入的資源。 ②然後執行module本身。 ps:內聯module少了下載module本身的步驟,其他步驟和引入的module相同。 |
預設是下載完成立即執行 |