JavaScript模塊化原理深入分析
1. 為什么需要 Javascipt 模塊化
- 解決命名沖突。將所有變量都掛載在到全局
global會引用命名沖突的問題。模塊化可以把變量封裝在模塊內(nèi)部。 - 解決依賴管理。Javascipt 文件如果存在相互依賴的情況就需要保證被依賴的文件先被加載。使用模塊化則無需考慮文件加載順序。
- 按需加載。如果引用 Javascipt 文件較多,同時加載會花費加多時間。使用模塊化可以在文件被依賴的時候被加載,而不是進入頁面統(tǒng)一加載。
- 代碼封裝。將相同功能代碼封裝起來方便后續(xù)維護和復用。
2. 你知道哪幾種模塊化規(guī)范
CommonJS
Node.js 采用了 CommonJS 模塊規(guī)范。
CommonJS 規(guī)范規(guī)定,每個模塊內(nèi)部,module 變量代表當前模塊。這個變量是一個對象,它的 exports 屬性(即 module.exports )是對外的接口。加載某個模塊,其實是加載該模塊的 module.exports 屬性。使用 require 方法加載模塊。模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
引入模塊得到的值其實是模塊輸出值的拷貝,如果是復雜對象則為淺拷貝。
// a.js
let count = 1;
function inc() {
count++;
}
module.exports = {
count: count,
inc: inc
};
// b.js
const a = require('./a.js');
console.log(a.count); // 1
a.inc();
console.log(a.count); // 1
因為 CommonJS 輸出的是值的淺拷貝,也就是說 count 在輸出后就不再和原模塊的 count 有關聯(lián)。
在 Node 中每一個模塊都是一個對象,其有一個 exports 屬性,就是文件中指定的 module.exports,當我們通過 require 獲取模塊時,得到的就是 exports 屬性。再看另一個例子:
// a.js
module.exports = 123;
setTimeout(() => {
module.exports = 456;
}, 1000);
// b.js
console.log(require('./a.js')); // 123
setTimeout(() => {
console.log(require('./a.js')); // 456
}, 2000);
模塊的 module.exports 值改變了,我們通過 require 獲取模塊的值也會發(fā)生變化。
CommonJS 使用了同步加載,即加載完成后才進行后面的操作,所以比較適合服務端,如果用在瀏覽器則可能導致頁面假死。
AMD
AMD(Asynchronous Module Definition,異步加載模塊定義)。這里異步指的是不堵塞瀏覽器其他任務(dom構建,css渲染等),而加載內(nèi)部是同步的(加載完模塊后立即執(zhí)行回調(diào))。 AMD 也采用 require 命令加載模塊,但是不同于 CommonJS,它要求兩個參數(shù),依賴模塊和回調(diào):
require([module], callback);
以 RequireJS 示例, 具體語法可以參考 requirejs.org/
簡單提供一下代碼示例,方便后續(xù)理解。
定義兩個模塊 calc 和 log 模塊
// calc.js
define(function(require, factory) {
function add(...args) {
return args.reduce((prev, curr) => prev + curr, 0);
}
return {
add
}
});
// log.js
define(function(require, factory) {
function log(...args) {
console.log('---log.js---');
console.log(...args)
}
return log
});
在 index.js 中引用兩個模塊
require(['./calc.js', './log.js'], function (calc, log) {
log(calc.add(1,2,3,4,5));
});
在 HTML 中引用
<script src="./require.js"></script> <script src="./index.js"></script>
可以看到在被依賴模塊加載完成后會把返回值作為依賴模塊的參數(shù)傳入,在被加載模塊全部執(zhí)行完成后可以去執(zhí)行加載模塊。
UMD
UMD(Universal Module Definition,通用模塊定義),所謂的通用,就是兼容了 CommonJS 和 AMD 規(guī)范,這意味著無論是在 CommonJS 規(guī)范的項目中,還是 AMD 規(guī)范的項目中,都可以直接引用 UMD 規(guī)范的模塊使用。
原理其實就是在模塊中去判斷全局是否存在 exports 和 define,如果存在 exports,那么以 CommonJS 的方式暴露模塊,如果存在 define 那么以 AMD 的方式暴露模塊:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.Requester = factory(root.$, root._);
}
}(this, function ($, _) {
// this is where I defined my module implementation
const Requester = { // ... };
return Requester;
}));
ESM (ES6 模塊)
CommonJS 和 AMD 模塊,都只能在運行時確定輸入輸出,而 ES6 模塊是在編譯時就能確定模塊的輸入輸出,模塊的依賴關系。
在 Node.js 中使用 ES6 模塊需要在 package.json 中指定 {"type": "module"}。
在瀏覽器環(huán)境使用 ES6 模塊需要指定 <script type="module" src="module.js"></script>
ES6 模塊通過 import 和 export 進行導入導出。ES6 模塊中 import 的值是原始值的動態(tài)只讀引用,即原始值發(fā)生變化,引用值也會變化。
import 命令具有提升效果,會提升到整個模塊的頭部,優(yōu)先執(zhí)行。
// a.js
export const obj = {
a: 5
}
// b.js
console.log(obj)
import { obj } from './a.js'
// 運行 b.js 輸出: { a: 5 }import,export 指定必須處理模塊頂層,也就是說不能在 if、for 等語句內(nèi)。下面這種使用方式是不合法的。
if (expr) {
import val from 'some_module'; // error!
}
UMD 通常是在 ESM 不起作用情況下備用,未來趨勢是瀏覽器和服務器都會支持 ESM。
由于 ES6 模塊是在編譯階段執(zhí)行的,可以更好的在編譯階段進行代碼優(yōu)化,如 Tree Shaking 就是依賴 ES6 模塊去靜態(tài)分析代碼而刪除無用代碼。
3. CommonJS 和 ES6 模塊的區(qū)別
- CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
- CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
- CommonJs 是單個值導出,ES6 Module可以導出多個
- CommonJs 是動態(tài)語法可以寫在判斷里,ES6 Module 靜態(tài)語法只能寫在頂層
- CommonJs 的 this 是當前模塊,ES6 Module的 this 是 undefined
4. CommonJS 和 AMD 實現(xiàn)原理
CommonJS
我們通過寫一個簡單的 demo 實現(xiàn) CommonJS 來理解其原理。
1、實現(xiàn)文件的加載和執(zhí)行
我們在用 Node.js 時都知道有幾個變量和函數(shù)是不需要引入可以直接使用的,就是 require(),__filename,__dirname,exports,module。這些變量都是 Node.js 在執(zhí)行文件時注入進去的。
舉個栗子,我們創(chuàng)建一個 add.js 文件,導出一個 add() 函數(shù):
function add(a, b) {
return a + b;
}
module.exports = add;
現(xiàn)在我們要加載并執(zhí)行這個文件,我們可以通過 fs.readFileSync 加載文件。
const fs = require("fs");
// 同步讀取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件內(nèi)容
我們要在執(zhí)行時傳入 require(),__filename,__dirname,exports,module 這幾個參數(shù),可以在一個函數(shù)中執(zhí)行這段代碼,而函數(shù)的參數(shù)就是這幾個參數(shù)即可。我們簡單的創(chuàng)建一個函數(shù),函數(shù)的內(nèi)容就是剛才我們加載的文件內(nèi)容,參數(shù)名依次是規(guī)范要求注入的幾個參數(shù)。
// 通過 new Function 生成函數(shù),參數(shù)分別是函數(shù)的入?yún)⒑秃瘮?shù)的內(nèi)容
const compiledWrapper = new Function(
"exports",
"require",
"module",
"__filename",
"__dirname",
data
);
現(xiàn)在我們執(zhí)行這個函數(shù),先不考慮 require,__filename 和 __dirname,只傳 exports 和 module。
const mymodule = {};
const myexports = (mymodule.exports = {});
// 執(zhí)行函數(shù)并傳入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null);
現(xiàn)在我們可以簡單的了解導出變量的原理,我們把 module 傳給函數(shù),在函數(shù)中,把需要導出的內(nèi)容掛在 module 上,我們就可以通過 module 獲取導出內(nèi)容了。
而 exports 只是 module.exports 的一個引用,我們可以給 module.exports 賦值,也可以通過 exports.xxx 形式賦值,這樣也相當于給 module.exports.xxx 賦值。但是如果直接給 exports 賦值將不生效,因為這樣 exports 就和 module 沒關系了,我們本質(zhì)上還是要把導出結果賦值給 module.exports 。
現(xiàn)在的完整代碼貼一下:
const fs = require("fs");
// 同步讀取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件內(nèi)容
// 創(chuàng)建函數(shù)
const compiledWrapper = new Function(
"exports",
"require",
"module",
"__filename",
"__dirname",
data
);
const mymodule = {};
const myexports = (mymodule.exports = {});
// 執(zhí)行函數(shù)并傳入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null);
console.log(mymodule, myexports, mymodule.exports(1, 2));
// { exports: [Function: add] } {} 3我們可以獲取了 add 函數(shù),并成功調(diào)用。
2、引用文件
我們剛才已經(jīng)成功加載并執(zhí)行了文件,如何在另一個文件通過 require 引用呢。其實就是把上面的操作封裝一下。
不過現(xiàn)在我們把參數(shù)全部傳進去,require,__filename 和 __dirname,分別是我們當前實現(xiàn)的 require 函數(shù),加載文件的文件路徑,加載文件的目錄路徑。
const fs = require('fs');
const path = require('path');
function _require(filename) {
// 同步讀取文件
const data = fs.readFileSync(filename, 'utf8'); // 文件內(nèi)容
const compiledWrapper = new Function(
'exports',
'require',
'module',
'__filename',
'__dirname',
data
);
const mymodule = {};
const myexports = (mymodule.exports = {});
const _filename = path.resolve(filename)
const _dirname = path.dirname(_filename);
compiledWrapper.call(myexports, _require, myexports, mymodule, _filename, _dirname);
return mymodule.exports
}
const add = _require('./add.js')
console.log(add(12, 13)); // 25
3、模塊緩存
現(xiàn)在就實現(xiàn)了文件的加載和引用,現(xiàn)在還差一點,就是緩存。之前說過,一個模塊只會加載一次,然后在全局緩存起來,所以需要在全局保存緩存對象。
// add.js
console.log('[add.js] 加載文件....')
function add(a, b) {
return a + b;
}
module.exports = add;
// require.js
const fs = require('fs');
const path = require('path');
// 把緩存對象原型設置為null 防止通過原型鏈查到同名的key (比如一個模塊叫 toString
const _cache = Object.create(null);
function _require(filename) {
const cachedModule = _cache[filename];
if (cachedModule) {
// 如果存在緩存就直接返回
return cachedModule.exports;
}
// 同步讀取文件
const data = fs.readFileSync(filename, 'utf8'); // 文件內(nèi)容
const compiledWrapper = new Function(
'exports',
'require',
'module',
'__filename',
'__dirname',
data
);
const mymodule = {};
const myexports = (mymodule.exports = {});
const _filename = path.resolve(filename);
const _dirname = path.dirname(_filename);
compiledWrapper.call(
myexports,
_require,
myexports,
mymodule,
_filename,
_dirname
);
_cache[filename] = mymodule;
return mymodule.exports;
}
const add1 = _require('./add.js');
const add2 = _require('./add.js');
console.log(add1(12, 13)); // [add.js] 加載文件.... 25
console.log(add2(13, 14)); // 27可以看到加了緩存后,引用了兩次模塊,但只加載了一次。
一個簡單的 CommonJS 規(guī)范實現(xiàn)就完成了。
AMD
上面提供了 RequireJS 的示例代碼,打開控制臺可以發(fā)現(xiàn) HTML 中被添加了兩個 <script> 標簽,引入了程序中依賴的兩個文件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8""> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Document</title> <script type="text/javascript" charset="utf-8" async data-requirecontext="_"data-requiremodule="A" src="././calc.js "></script> <script type="text/javascript" charset="utf-8" async data-requirecontext="_"data-requiremodule="B" src="././log.js "></script> </head> <body> == $0 <script src=" . /require.js"></script> <script src=" . / index.js"></script> </body> </html>
這樣我們可以推測 RequireJS 的實現(xiàn)原理,就是在執(zhí)行程序的過程中,發(fā)現(xiàn)依賴文件未被引用,就在 HTML 中插入一個 <script> 節(jié)點引入文件。
這里涉及一個知識點,我們可以看到被 RequireJS 插入的標簽都設置了 async 屬性。
- 如果我們直接使用
script腳本的話,HTML 會按照順序來加載并執(zhí)行腳本,在腳本加載&執(zhí)行的過程中,會阻塞后續(xù)的DOM渲染。 - 如果設置了
async,腳本會異步加載,并在加載完成后立即執(zhí)行。 - 如果設置了
defer,瀏覽器會異步的下載文件并且不會影響到后續(xù)DOM的渲染,在文檔渲染完畢后,DOMContentLoaded事件調(diào)用前執(zhí)行,按照順序執(zhí)行所有腳本。
所以我們可以推測 RequireJS 原理,通過引入 <script> 標簽異步加載依賴文件,等依賴文件全部加載完成,把文件的輸入作為參數(shù)傳入依賴文件。
到此這篇關于JavaScript模塊化原理深入分析的文章就介紹到這了,更多相關JS模塊化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JS中Generator函數(shù)與async函數(shù)用法介紹
javascript中經(jīng)常會用到異步編程,在ES6之后我們使用的?Generator函數(shù)、async函數(shù)、promise都是我們異步編程的一大助力,這里我們主要講解Generator、async函數(shù),并且簡介他們之間的一些聯(lián)系,本篇文章會帶著一些簡易案例,方便大家理解使用2023-06-06

