天天看點

從零開始學_JavaScript_系列(66)——AMD、CMD、CommonJS

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);

參數一和參數二可以省略,參數三是必須的。

參數一:子產品名(字元串)。有沒有随意,沒有的話會自動給一個預設的;

參數二:依賴(數組)。可選,如果需要依賴的話,會等這些依賴加載完後,再去執行參數三。

參數三:工廠方法,可以是對象,也可以是函數,核心内容。

  1. 如果是對象,那麼此對象為子產品的輸出值;
  2. 如果是函數,那麼應該隻被執行一次(且那些依賴會作為參數傳入);
  3. 如果是函數且有傳回值(隐式轉換非false),那麼這個傳回值就是子產品的輸出值;

簡單總結一下,通過

define

函數,可以加載一些依賴,全加載完之後,會執行一個函數(依賴作為參數傳入)。如果這個函數沒值傳回,那就單純隻是執行而已,如果有值傳回,那麼這個值就是本子產品的輸出值。

先附一個符合AMD規範的函數:

define(['3'], function (obj) {
    return Object.assign({}, obj, {b: })
})
           

再上一個帶AMD子產品自制加載器,完整注釋的模拟異步加載的代碼,有以下特點:

  1. 根元件異步加載2個子產品;
  2. 異步的子產品之一,也異步加載第三個子產品;
  3. 三個子產品情況各不同,分别是:①隻有參數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規範的差別有:

  1. 同步加載,類似Common.js(性能好);AMD是依賴前置,提前執行了(使用者體驗好);
  2. 按順序加載,放在代碼前的先加載,代碼後的後加載——依賴前置;AMD的是依賴加載完後,哪個依賴先下載下傳完就先加載和執行哪個依賴——依賴就近;
  3. CMD是就近原則,先下載下傳好依賴,然後需要執行的時候再解析(延遲執行);AMD是下載下傳好就立刻解析(提前執行);

上CMD的核心函數:

define(id?, deps?, factory);

是不是覺得和AMD的很像,事實也如此,defined的參數基本是一緻的,差別在于factory的參數。

factory(require, exports, module) {//…}
  1. require類似Common.js的require,但他不負責去同步加載(因為子產品已經加載好了),他主要負責解析。另外,嚴格符合要求的話,這裡的參數名必須是require,而不是是其他的,參照require 書寫約定,以及為什麼要有約定和建構工具;
  2. exports類似Common.js的module.exports,用于向外提供接口。他是參數三module.exports的一個引用,是以不能直接給他指派另外一個對象(按引用傳遞,不會影響module.exports);
  3. module是一個對象,上面存儲了與目前子產品相關聯的一些屬性和方法;

由于加載器的實作邏輯比較複雜,是以我隻能暫時略掉了。但是給一個參考連結吧:如何實作一個 CMD 子產品加載器,以及他的github

再補一個知乎的答案:JS子產品加載器加載原理是怎麼樣的?JZeng的答案

簡單來說:

  1. 通過子產品依賴清單,去加載該子產品的每個依賴(每個依賴也是一個子產品),然後子子產品也去加載他自己的依賴;
  2. 當子產品的依賴加載完畢後,通報給他的父子產品,而父子產品當所有他的子子產品加載完畢後,再通報父子產品的父子產品;
  3. 這樣形成一個樹的展開,再随着狀态的完成(或錯誤),再收斂到根子產品;
  4. 當所有子產品加載完成後,根子產品的狀态也會變成完成,然後開始執行根子產品的factory;