JavaScript模塊化原理深入分析
1. 為什么需要 Javascipt 模塊化
- 解決命名沖突。將所有變量都掛載在到全局
global會(huì)引用命名沖突的問題。模塊化可以把變量封裝在模塊內(nèi)部。 - 解決依賴管理。Javascipt 文件如果存在相互依賴的情況就需要保證被依賴的文件先被加載。使用模塊化則無需考慮文件加載順序。
- 按需加載。如果引用 Javascipt 文件較多,同時(shí)加載會(huì)花費(fèi)加多時(shí)間。使用模塊化可以在文件被依賴的時(shí)候被加載,而不是進(jìn)入頁面統(tǒng)一加載。
- 代碼封裝。將相同功能代碼封裝起來方便后續(xù)維護(hù)和復(fù)用。
2. 你知道哪幾種模塊化規(guī)范
CommonJS
Node.js 采用了 CommonJS 模塊規(guī)范。
CommonJS 規(guī)范規(guī)定,每個(gè)模塊內(nèi)部,module 變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,它的 exports 屬性(即 module.exports )是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的 module.exports 屬性。使用 require 方法加載模塊。模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
引入模塊得到的值其實(shí)是模塊輸出值的拷貝,如果是復(fù)雜對(duì)象則為淺拷貝。
// 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
因?yàn)?CommonJS 輸出的是值的淺拷貝,也就是說 count 在輸出后就不再和原模塊的 count 有關(guān)聯(lián)。
在 Node 中每一個(gè)模塊都是一個(gè)對(duì)象,其有一個(gè) exports 屬性,就是文件中指定的 module.exports,當(dāng)我們通過 require 獲取模塊時(shí),得到的就是 exports 屬性。再看另一個(gè)例子:
// 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 獲取模塊的值也會(huì)發(fā)生變化。
CommonJS 使用了同步加載,即加載完成后才進(jìn)行后面的操作,所以比較適合服務(wù)端,如果用在瀏覽器則可能導(dǎo)致頁面假死。
AMD
AMD(Asynchronous Module Definition,異步加載模塊定義)。這里異步指的是不堵塞瀏覽器其他任務(wù)(dom構(gòu)建,css渲染等),而加載內(nèi)部是同步的(加載完模塊后立即執(zhí)行回調(diào))。 AMD 也采用 require 命令加載模塊,但是不同于 CommonJS,它要求兩個(gè)參數(shù),依賴模塊和回調(diào):
require([module], callback);
以 RequireJS 示例, 具體語法可以參考 requirejs.org/
簡(jiǎn)單提供一下代碼示例,方便后續(xù)理解。
定義兩個(gè)模塊 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 中引用兩個(gè)模塊
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>
可以看到在被依賴模塊加載完成后會(huì)把返回值作為依賴模塊的參數(shù)傳入,在被加載模塊全部執(zhí)行完成后可以去執(zhí)行加載模塊。
UMD
UMD(Universal Module Definition,通用模塊定義),所謂的通用,就是兼容了 CommonJS 和 AMD 規(guī)范,這意味著無論是在 CommonJS 規(guī)范的項(xiàng)目中,還是 AMD 規(guī)范的項(xiàng)目中,都可以直接引用 UMD 規(guī)范的模塊使用。
原理其實(shí)就是在模塊中去判斷全局是否存在 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 模塊,都只能在運(yùn)行時(shí)確定輸入輸出,而 ES6 模塊是在編譯時(shí)就能確定模塊的輸入輸出,模塊的依賴關(guān)系。
在 Node.js 中使用 ES6 模塊需要在 package.json 中指定 {"type": "module"}。
在瀏覽器環(huán)境使用 ES6 模塊需要指定 <script type="module" src="module.js"></script>
ES6 模塊通過 import 和 export 進(jìn)行導(dǎo)入導(dǎo)出。ES6 模塊中 import 的值是原始值的動(dòng)態(tài)只讀引用,即原始值發(fā)生變化,引用值也會(huì)變化。
import 命令具有提升效果,會(huì)提升到整個(gè)模塊的頭部,優(yōu)先執(zhí)行。
// a.js
export const obj = {
a: 5
}
// b.js
console.log(obj)
import { obj } from './a.js'
// 運(yùn)行 b.js 輸出: { a: 5 }import,export 指定必須處理模塊頂層,也就是說不能在 if、for 等語句內(nèi)。下面這種使用方式是不合法的。
if (expr) {
import val from 'some_module'; // error!
}
UMD 通常是在 ESM 不起作用情況下備用,未來趨勢(shì)是瀏覽器和服務(wù)器都會(huì)支持 ESM。
由于 ES6 模塊是在編譯階段執(zhí)行的,可以更好的在編譯階段進(jìn)行代碼優(yōu)化,如 Tree Shaking 就是依賴 ES6 模塊去靜態(tài)分析代碼而刪除無用代碼。
3. CommonJS 和 ES6 模塊的區(qū)別
- CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。
- CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。
- CommonJs 是單個(gè)值導(dǎo)出,ES6 Module可以導(dǎo)出多個(gè)
- CommonJs 是動(dòng)態(tài)語法可以寫在判斷里,ES6 Module 靜態(tài)語法只能寫在頂層
- CommonJs 的 this 是當(dāng)前模塊,ES6 Module的 this 是 undefined
4. CommonJS 和 AMD 實(shí)現(xiàn)原理
CommonJS
我們通過寫一個(gè)簡(jiǎn)單的 demo 實(shí)現(xiàn) CommonJS 來理解其原理。
1、實(shí)現(xiàn)文件的加載和執(zhí)行
我們?cè)谟?Node.js 時(shí)都知道有幾個(gè)變量和函數(shù)是不需要引入可以直接使用的,就是 require(),__filename,__dirname,exports,module。這些變量都是 Node.js 在執(zhí)行文件時(shí)注入進(jìn)去的。
舉個(gè)栗子,我們創(chuàng)建一個(gè) add.js 文件,導(dǎo)出一個(gè) add() 函數(shù):
function add(a, b) {
return a + b;
}
module.exports = add;
現(xiàn)在我們要加載并執(zhí)行這個(gè)文件,我們可以通過 fs.readFileSync 加載文件。
const fs = require("fs");
// 同步讀取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件內(nèi)容
我們要在執(zhí)行時(shí)傳入 require(),__filename,__dirname,exports,module 這幾個(gè)參數(shù),可以在一個(gè)函數(shù)中執(zhí)行這段代碼,而函數(shù)的參數(shù)就是這幾個(gè)參數(shù)即可。我們簡(jiǎn)單的創(chuàng)建一個(gè)函數(shù),函數(shù)的內(nèi)容就是剛才我們加載的文件內(nèi)容,參數(shù)名依次是規(guī)范要求注入的幾個(gè)參數(shù)。
// 通過 new Function 生成函數(shù),參數(shù)分別是函數(shù)的入?yún)⒑秃瘮?shù)的內(nèi)容
const compiledWrapper = new Function(
"exports",
"require",
"module",
"__filename",
"__dirname",
data
);
現(xiàn)在我們執(zhí)行這個(gè)函數(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)在我們可以簡(jiǎn)單的了解導(dǎo)出變量的原理,我們把 module 傳給函數(shù),在函數(shù)中,把需要導(dǎo)出的內(nèi)容掛在 module 上,我們就可以通過 module 獲取導(dǎo)出內(nèi)容了。
而 exports 只是 module.exports 的一個(gè)引用,我們可以給 module.exports 賦值,也可以通過 exports.xxx 形式賦值,這樣也相當(dāng)于給 module.exports.xxx 賦值。但是如果直接給 exports 賦值將不生效,因?yàn)檫@樣 exports 就和 module 沒關(guān)系了,我們本質(zhì)上還是要把導(dǎo)出結(jié)果賦值給 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í)行了文件,如何在另一個(gè)文件通過 require 引用呢。其實(shí)就是把上面的操作封裝一下。
不過現(xiàn)在我們把參數(shù)全部傳進(jìn)去,require,__filename 和 __dirname,分別是我們當(dāng)前實(shí)現(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)在就實(shí)現(xiàn)了文件的加載和引用,現(xiàn)在還差一點(diǎn),就是緩存。之前說過,一個(gè)模塊只會(huì)加載一次,然后在全局緩存起來,所以需要在全局保存緩存對(duì)象。
// 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');
// 把緩存對(duì)象原型設(shè)置為null 防止通過原型鏈查到同名的key (比如一個(gè)模塊叫 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可以看到加了緩存后,引用了兩次模塊,但只加載了一次。
一個(gè)簡(jiǎn)單的 CommonJS 規(guī)范實(shí)現(xiàn)就完成了。
AMD
上面提供了 RequireJS 的示例代碼,打開控制臺(tái)可以發(fā)現(xiàn) HTML 中被添加了兩個(gè) <script> 標(biāo)簽,引入了程序中依賴的兩個(gè)文件。
<!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>
這樣我們可以推測(cè) RequireJS 的實(shí)現(xiàn)原理,就是在執(zhí)行程序的過程中,發(fā)現(xiàn)依賴文件未被引用,就在 HTML 中插入一個(gè) <script> 節(jié)點(diǎn)引入文件。
這里涉及一個(gè)知識(shí)點(diǎn),我們可以看到被 RequireJS 插入的標(biāo)簽都設(shè)置了 async 屬性。
- 如果我們直接使用
script腳本的話,HTML 會(huì)按照順序來加載并執(zhí)行腳本,在腳本加載&執(zhí)行的過程中,會(huì)阻塞后續(xù)的DOM渲染。 - 如果設(shè)置了
async,腳本會(huì)異步加載,并在加載完成后立即執(zhí)行。 - 如果設(shè)置了
defer,瀏覽器會(huì)異步的下載文件并且不會(huì)影響到后續(xù)DOM的渲染,在文檔渲染完畢后,DOMContentLoaded事件調(diào)用前執(zhí)行,按照順序執(zhí)行所有腳本。
所以我們可以推測(cè) RequireJS 原理,通過引入 <script> 標(biāo)簽異步加載依賴文件,等依賴文件全部加載完成,把文件的輸入作為參數(shù)傳入依賴文件。
到此這篇關(guān)于JavaScript模塊化原理深入分析的文章就介紹到這了,更多相關(guān)JS模塊化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于對(duì)TypeScript泛型參數(shù)的默認(rèn)值理解
泛型可以理解為寬泛的類型,通常用于類和函數(shù),下面這篇文章主要給大家介紹了關(guān)于對(duì)TypeScript泛型參數(shù)默認(rèn)值的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
JavaScript實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)可視化的示例詳解
動(dòng)態(tài)數(shù)據(jù)可視化能夠?qū)⒋罅繑?shù)據(jù)以直觀、生動(dòng)的方式呈現(xiàn),幫助用戶更好地理解和分析數(shù)據(jù),本文主要為大家介紹了如何使用JavaScript實(shí)現(xiàn)這一功能,需要的可以參考下2024-02-02
IE7中javascript操作CheckBox的checked=true不打勾的解決方法
在IE7下,生成的Checkbox無法正確的打上勾。 原因是 chkbox控件還沒初始化(appendChild),就開始操作它的結(jié)果2009-12-12
JavaScript必備的斷點(diǎn)調(diào)試技巧總結(jié)(推薦)
打斷點(diǎn)操作很簡(jiǎn)單,核心的問題在于,斷點(diǎn)怎么打才能夠排查出代碼的問題所在呢?下面這篇文章主要給大家總結(jié)介紹了關(guān)于JavaScript必備的斷點(diǎn)調(diào)試技巧,需要的朋友可以參考下2021-09-09

