1、CommonJS
先給一個CommonJS規範的連結。
簡單概述一下:
導出:
通過
module.exports
來導出。
module.exports
可以了解為一個對象,你可以把要導出的東西挂載在這個對象上。比如
module.exports.foo = 'a'
或者
module.exports = {
foo(){
console.log('foo')
},
bar: 'bar'
}
導入:
通過
var obj = require(子產品名)
來導入。
效果相當于
obj = module.exports
這樣。
使用示例:
建立foo.js檔案,裡面内容為:
module.exports = {
foo(){
console.log('foo')
},
bar: 'bar'
}
建立bar.js檔案,裡面内容為:
let test = require('./foo')
test.foo()
console.log(test.bar)
打開指令行,確定你安裝了Nodejs,然後運作:
輸出結果:
foo
bar
具體細節看本節開頭的連結。
另附一個我自己寫的模拟CommonJS規範的CommonJS極簡易加載器,附上。
function require (url) {
// 去拿取加載的資訊,這裡假定擷取到的是字元串
let text = sync(url)
// 前後添加一些字元串,封裝成一個函數,用括号括起來表示是一個表達式
text = '(function(module){' + text + '})'
// eval這個字元串,把他變為一個真函數
let fn = eval(text)
// 調用這個函數,并将module這個對象作為參數傳進去
// 這樣module就可以拿到導出的資料了
let module = {}
fn(module)
// 我們是通過module.exports導出的,是以傳回他
return module.exports
}
// 因為是CommonJS是同步的,是以用本函數模拟同步加載
function sync (url) {
let str = {
foo: `let bar = require('bar')
module.exports = {
foo () {
console.log('foo')
},
bar: bar
}`,
bar: `module.exports = 'bar'`
}
// 省去加載檔案的過程,直接輸出傳回結果
return str[url]
}
let myModel = require('foo')
myModel.foo()
console.log(myModel.bar)
2、AMD規範
先上一個AMD規範文檔的中文版
再附一個AMD加載器分析與實作
如果看上去還比較糊塗,那麼就繼續看下面我的解釋:
首先上AMD規範的核心:
define(id?, dependencies?, factory);
參數一和參數二可以省略,參數三是必須的。
參數一:子產品名(字元串)。有沒有随意,沒有的話會自動給一個預設的;
參數二:依賴(數組)。可選,如果需要依賴的話,會等這些依賴加載完後,再去執行參數三。
參數三:工廠方法,可以是對象,也可以是函數,核心内容。
- 如果是對象,那麼此對象為子產品的輸出值;
- 如果是函數,那麼應該隻被執行一次(且那些依賴會作為參數傳入);
- 如果是函數且有傳回值(隐式轉換非false),那麼這個傳回值就是子產品的輸出值;
簡單總結一下,通過
define
函數,可以加載一些依賴,全加載完之後,會執行一個函數(依賴作為參數傳入)。如果這個函數沒值傳回,那就單純隻是執行而已,如果有值傳回,那麼這個值就是本子產品的輸出值。
先附一個符合AMD規範的函數:
define(['3'], function (obj) {
return Object.assign({}, obj, {b: })
})
再上一個帶AMD子產品自制加載器,完整注釋的模拟異步加載的代碼,有以下特點:
- 根元件異步加載2個子產品;
- 異步的子產品之一,也異步加載第三個子產品;
- 三個子產品情況各不同,分别是:①隻有參數3,值是對象;②有依賴,且參數三是函數并有傳回值;③隻有參數三,是函數,有傳回值;
// 接受3個參數,分别是id(可選),依賴(可選),工廠函數/對象
function define() {
let array = [...arguments] // 參數直接用...array可以省略本行
let id, dependencies, factory;
if (array.length > ) {
[id, dependencies, factory] = array
} else if (array.length > ) {
[dependencies, factory] = array
} else {
[factory] = array
}
// 略過id,太麻煩不寫,也不考慮緩存什麼的
// 實際使用的話,可以通過目前script标簽的document.currentScript.src屬性擷取路徑并設為id
// 如果有依賴,先加載依賴
if (dependencies) {
// 建立一個空數組備用
let deArr = []
dependencies.forEach(item => {
// ajaxGet函數是傳回一個Promise對象
// 是以deArr變成一個放滿Promise對象的數組
deArr.push(ajaxGet(item))
})
// 利用Promise.all的特性
// 隻有當deArr裡面所有Promise對象的值都變為resolve才會執行all的then
// 具體原理請谷歌
return Promise.all(deArr).then(array => {
// deArray的所有結果被放到數組裡然後傳回
// 由于異步請求到的被當做字元串處理,是以先要eval解析
// funArray是放解析後的結果,每個元素都是Promise對象
// 因為define函數的傳回值是Promise對象
let funArray = array.map(str => {
return eval(str)
})
// 這個時候,Promise對象的狀态還沒變化,
// 是以用all的特性,等待他們全部狀态變化為正常的,再執行then
return Promise.all(funArray).then(result => {
// 這次有結果了,result是每個依賴的define函數的傳回值
// 如果是函數,那麼執行函數,并傳回函數結果
if (typeof factory === 'function') {
return factory(...result)
}
// 如果不是函數,那麼直接傳回參數3的值
return factory
})
})
}
// 是函數,則直接執行函數(因為沒有依賴)
if (typeof factory === 'function') {
return factory()
}
// 不是函數,則直接傳回參數三的值
return factory
}
// 模拟請求依賴
function ajaxGet(sign) {
// 簡化代碼,隻考慮成功,這個是模拟通過ajax去加載腳本
// 另外一個加載方式是通過createElement('script'),然後将script标簽插入頁面
return new Promise(resolve => {
setTimeout(() => {
let str = {
// 依賴1隻有一個參數3,值是對象
'1': 'define({a: 1})',
// 依賴2有一個自己的依賴(依賴3),值是函數
'2': `define(['3'], function (obj) {return Object.assign({}, obj, {b: })})`,
// 依賴3是一個函數,他有傳回值
'3': `define(function () {return {c: }})`
}
resolve(str[sign])
}, )
})
}
// 這是root
define(['1', '2'], function () {
console.log(arguments)
// {a:1}
// {c:3, b:2}
})
3、CMD規範
上面講了CommonJS和AMD規範,還剩下一個Sea.js推薦的CMD規範。
老規矩先上連結CMD規範。
另外友情提示一句,由于Sea.js停止維護,并且現代子產品更合适,是以新的項目裡,使用CMD規範的已經比較少了。但學習CMD規範有助于熟悉他的思想,對長期個人發展來說,還是有幫助的。
再另外提一句,大廠的面試題有時候會涉及到這個。
CMD規範和AMD規範的差別有:
- 同步加載,類似Common.js(性能好);AMD是依賴前置,提前執行了(使用者體驗好);
- 按順序加載,放在代碼前的先加載,代碼後的後加載——依賴前置;AMD的是依賴加載完後,哪個依賴先下載下傳完就先加載和執行哪個依賴——依賴就近;
- CMD是就近原則,先下載下傳好依賴,然後需要執行的時候再解析(延遲執行);AMD是下載下傳好就立刻解析(提前執行);
上CMD的核心函數:
define(id?, deps?, factory);
是不是覺得和AMD的很像,事實也如此,defined的參數基本是一緻的,差別在于factory的參數。
factory(require, exports, module) {//…}
- require類似Common.js的require,但他不負責去同步加載(因為子產品已經加載好了),他主要負責解析。另外,嚴格符合要求的話,這裡的參數名必須是require,而不是是其他的,參照require 書寫約定,以及為什麼要有約定和建構工具;
- exports類似Common.js的module.exports,用于向外提供接口。他是參數三module.exports的一個引用,是以不能直接給他指派另外一個對象(按引用傳遞,不會影響module.exports);
- module是一個對象,上面存儲了與目前子產品相關聯的一些屬性和方法;
由于加載器的實作邏輯比較複雜,是以我隻能暫時略掉了。但是給一個參考連結吧:如何實作一個 CMD 子產品加載器,以及他的github
再補一個知乎的答案:JS子產品加載器加載原理是怎麼樣的?JZeng的答案
簡單來說:
- 通過子產品依賴清單,去加載該子產品的每個依賴(每個依賴也是一個子產品),然後子子產品也去加載他自己的依賴;
- 當子產品的依賴加載完畢後,通報給他的父子產品,而父子產品當所有他的子子產品加載完畢後,再通報父子產品的父子產品;
- 這樣形成一個樹的展開,再随着狀态的完成(或錯誤),再收斂到根子產品;
- 當所有子產品加載完成後,根子產品的狀态也會變成完成,然後開始執行根子產品的factory;