平時寫程式碼的時候,知道如何匯出變數,如何引入變數。可見模組化就在我們的身邊,可是為什麼前端會引入模組化的概念,以及為什麼有同步載入和非同步載入呢?
為什麼要模組化
在之前的專案中,如果沒有模組化的概念,很多變數都有重名或者不小心重新賦值的危險。而且用 script 有可能阻塞 HTML 的下載或者渲染,影響使用者體驗。
在平時編碼中,我們都習慣把一些通用的方法提出來放在一個檔案裡,哪個地方需要用到就引用,這樣能夠很好的梳理頁面的邏輯,維護程式碼的成本也降低了不少。所以模組化給我們帶來的好處是顯而易見的。
- 分離: 程式碼需要分離成小塊,以便能為人所理解。
- 可組合性: 在一個檔案中編碼,被許多其他檔案重複使用。這提升了程式碼庫的靈活性。
- 解決全域性變數重名問題
- 提高複用性
現有的一些模組化方案有以下幾種:
- ES 6 模組
- Commonjs
- AMD
- CMD
下面我就自身的理解對這幾種方案做一個對比和總結:
ES6 Module
- 編譯時就能確定模組的依賴關係
ES6
模組遇到 import
命令時,不會去執行模組,而是生成一個引用,等用到的時候,才去模組中取值。因為是動態引用,所以不存在快取的問題。可以看一下下面的例子:
// util.js
export let env = 'qa';
setTimeout(() => env = 'local', 1000);
// main.js
import {env} from './util';
console.log('env:', env);
setTimeout(() => console.log('new env:', env), 1500);複製程式碼
執行 main.js
,會輸出下面的結果:
// env: qa
// new env: local複製程式碼
可以看出 ES6
模組是動態的取值,不會快取執行的結果。
目前瀏覽器尚未支援 ES6
模組 ,所以需要使用 babel 轉換,大家可以在 Babel 提供的 REPL 線上編譯器 中檢視編譯後的結果。
// es 6
import {add} from './config';
// es 5
'use strict';
var _config = require('./config');複製程式碼
可以看出,最後轉換成 require
的方式了。ES6
模組在瀏覽器和伺服器端都可以用,ES6
模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。
import
命令具有提升效果,會提升到整個模組的頭部,首先執行,所以不能把 import
寫在表示式裡面。這和 ES 6
模組的概念不符合。
CommonJS
- 用於伺服器端
- 只能在執行時確定
- 同步載入
- 模組載入的順序,按照其在程式碼中出現的順序。
node
的模組遵循 CommonJS
規範。在伺服器端,依賴是儲存在本地硬碟的,所以讀取的速度非常快,使用同步載入不會有什麼影響。
看一下 CommonJS
的語法:
// header.js
module.exports = {
title: '我是柚子'
};
// main.js
var header = require('./header');複製程式碼
module
這裡的 module 代表的是當前模組,它是一個物件,把它列印出來是下面的結果:
{
Module {
id: '/Users/yanmeng/2017FE/css-animation/js/b.js',
exports: { item: 'item' },
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/yanmeng/2017FE/css-animation/js/main.js',
loaded: false,
children: [ [Circular] ],
paths:
[ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
'/Users/yanmeng/2017FE/css-animation/node_modules',
'/Users/yanmeng/2017FE/node_modules',
'/Users/yanmeng/node_modules',
'/Users/node_modules',
'/node_modules' ] },
filename: '/Users/yanmeng/2017FE/css-animation/js/b.js',
loaded: false,
children: [],
paths:
[ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
'/Users/yanmeng/2017FE/css-animation/node_modules',
'/Users/yanmeng/2017FE/node_modules',
'/Users/yanmeng/node_modules',
'/Users/node_modules',
'/node_modules'
]
}複製程式碼
- id 是該模組的 id
- loaded 代表改模組是否載入完畢
- exports 是一個物件,裡面有模組輸出的各個介面。
之後呼叫這個模組的時候,就會從 exports 中取值,即使再執行,也不會再執行改模組,而是從快取中取值,返回的是第一次執行的結果,除非手動清除快取。
// 刪除指定模組的快取
delete require.cache[moduleName];
// 刪除所有模組的快取
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})複製程式碼
快取是根據絕對路徑識別模組的,如果同一個模組放在不同的路徑下,還是會重新載入這個模組。
require
require 命令第一次執行的時候,會載入並執行整個指令碼,然後在記憶體中生成此指令碼返回的 exports 物件。
ES6 模組與 CommonJS 模組的差異
- CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
- CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。
ES6
模組是動態引用,並且不會快取值。
ES6
模組在對指令碼靜態分析的時候,遇到 import
就會生成一個只讀引用,等到指令碼真正執行的時候,再根據這個只讀引用,到被載入的那個模組裡取值,所以說 ES6
模組是動態引用。
從依賴中引入的模組變數是一個地址引用,是隻讀的,可以為它新增屬性,可是不能重新賦值。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError複製程式碼
AMD
又稱非同步載入模組(Asynchronous Module Definition)
- 依賴前置
- 比較適合瀏覽器環境
- 實現
js
檔案的非同步載入,避免網頁失去響應- 管理模組之間的依賴性,便於程式碼的編寫和維護
- 代表庫: RequireJS
如果在瀏覽器環境,就需要在服務端載入模組,那麼採用同步載入的方法就會影響使用者體驗,所以瀏覽器端一般採用 AMD
規範。
它採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。
// lib.js
define('[./util.js]', function(util){
function bar() {
util.log('it is sunshine');
};
return {
bar: bar
};
});
// main.js
require(['./lib.js'], function(lib){
console.log(lib.bar());
})複製程式碼
CMD
- 依賴就近
- 需要用到依賴的時候才申明
- 代表庫: Sea.js
Sea.js
實現了這個規範,Sea.js
遇到依賴後只會去下載 JS
檔案,並不會執行,而是等到所有被依賴的 JS
指令碼都下載完以後,才從頭開始執行主邏輯。因此被依賴模組的執行順序和書寫順序完全一致。
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// ...
var b = require('./b')
b.doSomething()
// ...
})複製程式碼
本文只是淺顯的介紹了一些模組的概念和用法,關於 ES6 模組、 CommonJs 的迴圈載入和 ES 6 模組和 CommonJs的互相引用,大家可以動手實踐一下,會受益匪淺。
參考: