JavaScript模塊化原理深入分析
1. 為什么需要 Javascipt 模塊化
- 解決命名沖突。將所有變量都掛載在到全局
global
會引用命名沖突的問題。模塊化可以把變量封裝在模塊內部。 - 解決依賴管理。Javascipt 文件如果存在相互依賴的情況就需要保證被依賴的文件先被加載。使用模塊化則無需考慮文件加載順序。
- 按需加載。如果引用 Javascipt 文件較多,同時加載會花費加多時間。使用模塊化可以在文件被依賴的時候被加載,而不是進入頁面統(tǒng)一加載。
- 代碼封裝。將相同功能代碼封裝起來方便后續(xù)維護和復用。
2. 你知道哪幾種模塊化規(guī)范
CommonJS
Node.js 采用了 CommonJS 模塊規(guī)范。
CommonJS 規(guī)范規(guī)定,每個模塊內部,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渲染等),而加載內部是同步的(加載完模塊后立即執(zhí)行回調)。 AMD 也采用 require
命令加載模塊,但是不同于 CommonJS,它要求兩個參數(shù),依賴模塊和回調:
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
等語句內。下面這種使用方式是不合法的。
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"); // 文件內容
我們要在執(zhí)行時傳入 require()
,__filename
,__dirname
,exports
,module
這幾個參數(shù),可以在一個函數(shù)中執(zhí)行這段代碼,而函數(shù)的參數(shù)就是這幾個參數(shù)即可。我們簡單的創(chuàng)建一個函數(shù),函數(shù)的內容就是剛才我們加載的文件內容,參數(shù)名依次是規(guī)范要求注入的幾個參數(shù)。
// 通過 new Function 生成函數(shù),參數(shù)分別是函數(shù)的入?yún)⒑秃瘮?shù)的內容 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ù)中,把需要導出的內容掛在 module
上,我們就可以通過 module
獲取導出內容了。
而 exports
只是 module.exports
的一個引用,我們可以給 module.exports
賦值,也可以通過 exports.xxx
形式賦值,這樣也相當于給 module.exports.xxx
賦值。但是如果直接給 exports
賦值將不生效,因為這樣 exports
就和 module
沒關系了,我們本質上還是要把導出結果賦值給 module.exports
。
現(xiàn)在的完整代碼貼一下:
const fs = require("fs"); // 同步讀取文件 const data = fs.readFileSync("./add.js", "utf8"); // 文件內容 // 創(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ù),并成功調用。
2、引用文件
我們剛才已經成功加載并執(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'); // 文件內容 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'); // 文件內容 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
事件調用前執(zhí)行,按照順序執(zhí)行所有腳本。
所以我們可以推測 RequireJS 原理,通過引入 <script>
標簽異步加載依賴文件,等依賴文件全部加載完成,把文件的輸入作為參數(shù)傳入依賴文件。
到此這篇關于JavaScript模塊化原理深入分析的文章就介紹到這了,更多相關JS模塊化內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript實現(xiàn)動態(tài)數(shù)據(jù)可視化的示例詳解
動態(tài)數(shù)據(jù)可視化能夠將大量數(shù)據(jù)以直觀、生動的方式呈現(xiàn),幫助用戶更好地理解和分析數(shù)據(jù),本文主要為大家介紹了如何使用JavaScript實現(xiàn)這一功能,需要的可以參考下2024-02-02IE7中javascript操作CheckBox的checked=true不打勾的解決方法
在IE7下,生成的Checkbox無法正確的打上勾。 原因是 chkbox控件還沒初始化(appendChild),就開始操作它的結果2009-12-12