前端模塊化之AMD與CMD原理(附源碼)

1. 前言

如今初入前端的同窗們,都直接就上手webpack了,而在幾年前沒有node仍是jquery打天下的時候,不借助node或軟件讓不一樣js文件之間互相引用、模塊化開發,是件很麻煩的事。javascript

接下來會介紹兩個有名的工具AMD(require.js)和CMD(sea.js),雖然如今用的不多了,但仍是須要知道模塊化經歷的過程,面試也可能會問到。html

2. 需求的產生

一開始前端的工做內容很少,html中如圖中引入js文件,有不少缺點:

  1. 必須按順序引入,若是 1.js中要用到jquery,那就將jquery.js放到1.js上方。
  2. 同步加載各個js,只有1.js加載並執行完,纔去加載2.js。
  3. 各個js文件可能會有多個window全局變量的建立,污染。
  4. ......還有不少缺點

總之,上面的結構,在前端內容愈來愈多,尤爲ajax的趨勢、先後端分離、愈來愈注重前端體驗,js文件愈來愈多且互相引用更復雜的狀況下,真心亂套了,因此須要有一個新的模塊化工具。前端

咱們固然是但願像如今這樣,文件之間互相 import、export 就好了,但遺憾的是,這是es6配合node的用法,須要服務端作支撐處理文件,而一開始僅經過靜態文件去模塊化。java

3. AMD

即Asynchronous Module Definition,中文名是異步模塊定義的意思。它是一個模塊化開發的規範,是一種思路,是一個概念。咱們不用過於糾結它究竟是個啥,只須要知道它是一個規範概念,而大名鼎鼎的require.js,是它的一個具體的實現,之前不少都用這個工具開發。node

require.js的用法

這裏只介紹大概簡單的用法,沒用過的同窗最好去看教程。jquery

login.html中webpack

引入了require.js文件,而後指定了入口文件login.jses6

~~web

入口文件login.js中面試

loginModule.js中

loginCtrl.js中

注意:

  1. 實際上就是定義了兩個全局變量函數,一個require(),一個define()

  2. 其實這兩個函數功能和原理差很少,你能夠認爲他倆除了名字不同,其餘都差很少。

  3. define函數的第一個參數是個數組,寫的是所依賴的模塊;第二個參數是回調函數,回調函數裏的參數對應的是依賴數組裏的模塊返回值,如:

    es6 :

    import A from 'a.js'

    import B from 'b.js'

    require.js:

    define(['a.js', 'b.js'], function(A, B) {

    })

最須要注意的是!!!:

不管是CMD仍是AMD,都只是讓開發者寫代碼時變爽了,而對於瀏覽器來講,能夠認爲沒啥太大變化,該加載多少js文件,js文件的順序,都和之前沒啥區別!!!

4. require.js 原理

到這裏咱們大概知道了,這兩個工具的意義,是讓開發者再也不須要寫一堆 <script> 標籤 引入js了,讓編碼更爽。。。可是對於瀏覽器那邊,沒啥大的變化。

之前:

編碼:

瀏覽器:

require.js後

編碼:

瀏覽器:

因此結論:require.js只是採起了某種「方法」,讓你在寫代碼時只寫一個<script>,運行html文檔時倒是多個<script>

分析實現原理:

可能1:

編碼時的html文件和運行時的html文件是兩個文件,即經過某些工具複製並修改了html。惋惜修改文件須要服務端程序去作,而require.js只是個js文件,因此不是這個原理。這在node下webpack能夠輕鬆的實現。

可能2:

既然可能1是不對的,那麼說明了,瀏覽器運行的html文件和編碼時的html文件是如出一轍的。因此只剩下第二條路了,就是運行時由js代碼去修改html文檔~

工具目的:

因此咱們的目的就成了,瀏覽器文檔一開始運行時:

就只引用了一個require.js文件

執行了require.js以後,由js在dom上添加了一堆 <script>

也就是說,紅框裏的<script>,都是require.js裏的js代碼手動在body元素尾部添加的!!!

還有,添加了<script src="3.js" > , 不能只是在html文檔上添加了這行字符串,而是要加載而且運行 3.js !!!

!!!重要方法:

1. 插入sciprt節點,而且 加載 + 運行 其js文件

// 建一個node節點, script標籤
var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '3.js'

// 將script節點插入dom中
document.body.appendChild(node)

複製代碼

注意:

採用dom.appendChild方法插入script節點,會當即下載js文件,而且運行文件!

而採用dom.innerHTML = '<script src="3.js"></script>',則只是在dom中插入了一行字符串,就更不會管字符串裏引入的js了,因此不能用這個方法插入script!!!

2. 各個js文件的加載時機(script標籤插入文檔的時機順序)

文件之間有依賴關係的,因此加載js文件 (dom中插入script節點) 是要有順序的,好比: 1.js 依賴 2.js,2.js 依賴 3.js

那麼,實際的加載順序是爲 1.js,2.js,3.js

有同窗可能會問,按理說加載順序應該是反過來 3.js,2.js,1.js啊。。。那是由於,只有 1.js 加載而且運行以後,才知道1.js依賴啥啊。。。仔細想一想,你在 1.js 裏面寫得define([2.js]),那是否是得 先加載運行 1.js 後才能從define函數的參數中拿到依賴 2.js ?

注意: 而實際模塊運行的順序,纔是 3.js,2.js,1.js。。。因此,文件的加載、加載後文件的運行、模塊的運行,這是 3 個東西啊,別混了先,下一部分再細說。

繼續這裏,那麼就須要判斷,1.js何時加載完呢?

<script src="1.js" onload="alert()"></script>
複製代碼

關鍵就在於,onload 這個函數,其做用是,1.js 加載完而且執行完以後,執行onload裏的 alert

因此要實現 1.js 加載完後再加載 2.js ,則只需這樣:

var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '1.js'

// 給該節點添加onload事件,標籤上onload,這裏是load,見事件那裏的知識點
// 1.js 加載完後onload的事件
node.addEventListener('load', function(evt) {
    // 開始加載 2.js
    var node2 = document.createElement('script')
    node2.type = 'text/javascript'
    node2.src = '2.js'
    // 插入 2.js script 節點
    document.body.appendChild(node2)
})
// 插入 1.js script 節點
document.body.appendChild(node)

複製代碼

因此,處理依賴的核心就是利用 onload 事件,不斷的遞歸嵌套的加載依賴文件。事實上,最麻煩的也是這裏處理依賴文件,尤爲是一個文件可能被多個文件所依賴,的狀況。

3. 文件模塊的執行時機

這一點必定要理解,這也是require.js和sea.js的區別之一。

剛纔說了3個東西,文件的加載、加載後文件的運行、模塊的運行,這裏千萬別蒙圈,舉例:

// 1.js 中的代碼
require([], functionA() {
    // 主要邏輯代碼
})
複製代碼

js文件加載後就會瞬間執行文件,那麼

  • 文件的加載:將<script src='1.js' > 節點插入dom中,以後,下載 1.js 文件

  • 加載後文件的運行:1.js 文件加載完後,執行 1.js 中的代碼,即執行 require() 函數!!!

  • 模塊的運行: require回調函數,上方的,主要邏輯代碼,所在的函數,functionA,的運行!!!

因此咱們之後所說的 運行,都指的是 模塊的運行 ,而文件的運行默認和加載一塊兒了就,不需考慮。。。並且咱們每一個頁面的邏輯代碼都要是寫在require/define 的回調函數,functionA中的啊。。。

!!!必定要要仔細想清楚,模塊加載順序和模塊運行的順序?

就像 es6 你 1.js 中 import '2.js',不得 2.js 先執行而且返給你個值?即:

1.js 依賴 2.js 時,那麼是 1.js 先加載, 可是 2.js 模塊先運行(仍是要注意:是模塊的運行,不是文件的運行!!!)

總結順序:

  • 文件加載/文件運行 順序: 1.js , 2.js , 3.js

  • 模塊運行 順序:3.js , 2.js , 1.js

5. require.js 簡單代碼實現

用法例子

// 1.js 中(入口用require,其餘用define)
require(['2.js'], function(A) {
    // A獲得的就是2.js模塊的返回值
    // 主要的執行代碼
    // 2.js 3.js都加載完,才執行1.js的這回調函數!!!!!!!!!!!!!!!
})

// 2.js 中
define(['3.js', 'xxxx.js'], functionA(B, C) {
    // B獲得的就是3.js模塊的返回值,C是xxxx.js的
    return aaaaa    // 2.js 模塊的返回值
})

// 3.js 中
define([], functionA() {
    
    retrun {}   // 3.js 模塊的返回值
})

複製代碼

require.js 簡單源碼原理

利用遞歸去加載層層的嵌套依賴,代碼的難點就在於,怎樣判斷遞歸結束?即怎樣判斷全部的依賴都加載完了?

var modules = {},	// 存放全部文件模塊的信息,每一個js文件模塊的信息
    loadings = [];	//	存放全部已經加載了的文件模塊的id,一旦該id的全部依賴都
                                加載完後,該id將會在數組中移除

// 上面說了,每一個文件模塊都要有個id,這個函數是返回當前運行的js文件的文件名,拿文件名做爲文件對象的id
// 好比,當前加載 3.js 後運行 3.js ,那麼該函數返回的就是 '3.js'
function getCurrentJs() {
	return document.currentScript.src
}
// 建立節點
function createNode() {
	var node = document.createElement('script')
	node.type = 'text/javascript'
	node.async = true;
	return node
}
// 開始運行
function init() {
    // 加載 1.js
    loadJs('1.js')
}	
// 加載文件(插入dom中),若是傳了回調函數,則在onload後執行回調函數
function loadJs(url, callback) {
    var node = createNode()
    node.src = url;
    node.setAttribute('data-id', url)
    node.addEventListener('load', function(evt) {
    	var e = evt.target
    	setTimeout(() => {  // 這裏延遲一秒,只是讓在瀏覽器上直觀的看到每1秒加載出一個文件
    		callback && callback(e)
    	}, 1000)
    }, false)
    
    document.body.appendChild(node)
}	
	
// 此時,loadJs(1.js)後,並無傳回調函數,因此1.js加載成功後只是自動運行1.js代碼
// 而1.js代碼中,是require( ['2.js', 'xxx.js'], functionA(B, C){} ),則執行的是require函數, 在下面是require的定義

window.require = function(deps, callback) {
    // deps 就是對應的 ['2.js', 'xxx.js']
    // callback 就是對應的 functionA
    // 在這裏,是不會運行callback的(即模塊的運行!),得等到全部依賴都加載完的啊
    // 因此得有個地方,把一個文件的全部信息都先存起來啊,尤爲是deps和callback
    var id = getCurrentJs();// 當前運行的是1.js,因此id就是'1.js'
    if(!modules.id) {
    	modules[id] = { // 該模塊對象信息
    		id: id,
    		deps: deps,
    		callback: callback, 
    		exports: null,  // 該模塊的返回值return ,
    		就是functionA(B, C)運行後的返回值,仔細想一想?在後面的getExports中詳細講
    		
    		status: 1, 
    		
    	}
    	loadings.unshift(id); // 加入這個id,以後會循環loadings數組,遞歸判斷id全部依賴
    }
    
    loadDepsJs(id); // 加載這個文件的全部依賴,即去加載[2.js]
}

function loadDepsJs(id) {
    var module = modules[id]; // 獲取到這個文件模塊對象
    // deps是['2.js']
    module.deps.map(item => {   // item 實際上是依賴的Id,即 '2.js'
        if(!modules[i]) {   // 若是這個文件沒被加載過(注:加載過的確定在modules中有)
        (1)    loadJs(item, function() {   // 加載 2.js,而且傳了個回調,準備要遞歸了
                    // 2.js加載完後,執行了這個回調函數
                    loadings.unshift(item); // 此時裏面有兩個了, 1.js 和 2.js
                    // 遞歸。。。要去搞3.js了
                    loadDepsJs(item)// item傳的2.js,遞歸再進來時,就去modules中取2.js的deps了
                    // 每次檢查一下,是否都加載完了
                    checkDeps(); // 循環loadings,配合遞歸嵌套和modules信息,判斷是否都加載完了
                })
        }
    })
}

// 上面(1)那裏,加載了2.js後立刻會運行2.js的,而2.js裏面是
define(['js'], fn)
// 因此至關於執行了 define函數

window.define = function(deps,callback) {
    var id = getCurrentJs()
    if(!modules.id) {
        modules[id] = {
        	id: id,
        	deps: getDepsIds(deps),
        	callback: callback,
        	exports: null,
        	status: 1,
        	
        }
    }
}

// 注意,define運行的結果,只是在modules中添加了該模塊的信息
// 由於其實在上面的loadDepsJs中已經事先作了loadings和遞歸deps的操做,
並且是一直不斷的循環往復的進行探查,因此define裏面就不須要再像require中寫一次loadDeps了

// 循環loadings,查看loadings裏面的id,其所依賴的全部層層嵌套的依賴模塊是否都加載完了

function checkDeps() {
    for(var i = 0, id; i < loadings.length ; i++) {
	id = loadings[i]
	if(!modules[id]) continue
	
	var obj = modules[id], 
	deps = obj.deps
	
	// 下面那行爲何要執行checkCycle函數呢,checkDeps是循環loadings數組的模塊id,而checkCycle是去判斷該id模塊所依賴的**層級**的模塊是否加載完
	// 即checkDeps是**廣度**的循環已經加載(但依賴沒徹底加載完的)的id
	// checkCycle是**深度**的探查所關聯的依賴
	// 仍是舉例吧。。。假如除了1.js, 2.js, 3.js, 還有個4.js,依賴5.js,那麼
	// loadings 可能 是 ['1.js', '4.js']
	// 因此checkDeps --> 1.js,  4.js
	// checkCycle深刻內部 1.js --> 2.js --> 3.js ;;; 4.js --> 5.js
	// 一旦好比說1.js的全部依賴2.js、3.js都加載完了,那麼1.js 就會在loadings中移出
	
	var flag = checkCycle(deps)
	
	if(flag) {
            console.log(i, loadings[i] ,'所有依賴已經loaded');
		
            loadings.splice(i,1);
            // !!!運行模塊,而後同時獲得該模塊的返回值!!!
            getExport(obj.id)
            // 不斷的循環探查啊~~~~
            checkDeps()
	}
	
    }
}
// 深層次的遞歸的去判斷,層級依賴是否都加在完了
// 進入1.js的依賴2.js,再進入2.js的依賴3.js ......
function checkCycle(deps) {
   var flag = true
   
   function cycle(deps) {
        deps.forEach(item => {
             if(!modules[item] || modules[item].status == 1) {
                   flag = false
             } else if(modules[item].deps.length) {
//                         console.log('inner deps', modules[item].deps);
                   
                   cycle(modules[item].deps)
             }
                   
        })
   }
   
   cycle(deps)
   
   return flag
}

/*
    運行該id的模塊,同時獲得模塊返回值,modules[id].export
*/
function getExport(id) {
/*
    先想一下,例如模塊2.js, 這時 id == 2.js
    define(['3.js', 'xxxx.js'], functionA(B, C) {
        // B獲得的就是3.js模塊的返回值,C是xxxx.js的
        return aaaaa    // 2.js 模塊的返回值
    })
    因此:
    1. 運行模塊,就是運行 functionA (模塊的callback)
    2. 獲得模塊的返回值,就是functionA運行後的返回值 aaaaa
    問題:
    1. 運行functionA(B, C)   B, C是什麼?怎麼來的?
    2. 有B, C 了,怎麼運行functionA ?
    
*/
    // 解決問題1
    // B, C 就是該模塊依賴 deps [3.js, xxxx.js]對應的返回值啊 
    // 那麼循環deps 獲得 依賴模塊Id, 取模塊的export。。。
   var params = [];
   var deps = modules[id].deps  
   
   for(var i = 0; i < deps.length; i++) {
        // 取依賴模塊的exports即模塊返回值,注意不要懼怕取不到,由於你這個模塊
        都進來打算運行了,那麼你的全部依賴的模塊早都進來過運行完了(還記得模塊運行順序不?)
        let depId = deps[i]
        params.push( modules[ depId ].exports ) 
        
   }
   
   // 到這裏,params就是依賴模塊的返回值的數組,也就是B,C對應的實參
   // 也就是 params == [3.js的返回值,xxxx.js的返回值]
   
   if(!modules[id].exports) {
        // 解決問題2: callback(functionA)的執行,用.apply,這也是爲何params是個數組了
        // 這一行代碼,既運行了該模塊,同時也獲得了該模塊的返回值export
        modules[id].exports = modules[id].callback.apply(global, params)
   }
  
}
	
複製代碼

代碼的難點就在於checkDeps以及對loadings進行遞歸那裏,很難去講清楚,須要本身去寫去實踐,這裏也很難全都描述清楚。。。

結尾會給一個簡單的能運行的例子

不要想着花一兩個小時就搞定全部了,剛開始確實會看的煩,多回來幾回,隔段時間再研究一下,每次都會加深一點

6. CMD

CMD 即Common Module Definition通用模塊定義,sea.js是它的實現

sea.js是阿里的大神寫得,和require.js很像,先看一下用法的區別

// 只有define,沒有require
// 和AMD那個例子同樣,仍是1依賴2, 2依賴3
1.js中
define(function() {
    
    var a = require('2.js')
    console.log(33333)
    var b = require('4.js')
})

2.js 中
define(function() {
    var b = require('3.js')
})
3.js 中
define(function() {
    // xxx
})

複製代碼

看着是比require.js要好一點。。。

AMD和CMD的區別

對依賴模塊的執行時機不一樣,注意:不是加載的時機,模塊加載的時機是同樣的!!!

文件加載順序: 都是先加載1.js,再加載2.js,最後加載3.js

模塊運行順序

AMD: 3.js,2.js,1.js,,,即若是模塊以及該模塊的依賴都加載完了,那麼就執行。。。 好比 3.js 加載完後,發現本身也沒有依賴啊,那麼直接執行3.js的回調了,,,2.js加載完後探查到依賴的3.js也加載完了,那麼2.js就執行本身的回調了。。。。 主模塊必定在最後執行

CMD: 1.js,2.js,3.js,,,即先執行主模塊1.js,碰到require('2.js')就執行2.js,2.js中碰到require('3.js')就執行3.js

會不會又不理解,怎麼能控制執行哪一個文件模塊呢?啥時執行呢?

還記得不,以前說過,執行模塊,是指的執行那個functionA回調函數,callback,,,那麼這個callback函數其實在一開始執行define()中,就已經經過參數,賦到了modules上了啊,因此不管CMD仍是AMD,執行模塊,都是執行modules[id].callback()

因此,sea.js裏,你用的var a = require('2.js'),中的執行的require函數,源碼中就是簡單的執行了模塊的callback

7. sea.js源碼

源碼,大部分和require.js都很像,上面說的執行時機不一樣,也很簡單,就是控制一下啥時執行modules[id].callback唄。

以前又說了,加載模塊差很少,那麼sea.js是怎麼經過require(3.js),require(2.js)去控制3.js和2.js的加載呢???上面說require函數已經就是執行callback了,那麼require函數就不能承擔起加載模塊的功能了啊,再來看

CMD的define

用法

define(function() {
    var a = require('2.js')
})
複製代碼

define源碼的定義

window.define = function(callback) {
    var id = getCurrentJs()
    var depsInit = s.parseDependencies(callback.toString())
    var a = depsInit.map(item => basepath + item)
    // 和require.js的define相比,就多了上面的2行代碼
    // 1. 把傳進來的函數給轉換成字符串,'function (){var a = require("2.js")}'
    // 2. 利用一個正則函數,取出字符串中require中的2.js,最後拼成一個數組['2.js']返回來。
    // 3. 以後就和require.js差很少了啊。。。
    
    
    // 下面的都差很少
    if(!modules[id]) {
        modules[id] = {
            id: id,
            status: 1,
            callback: callback,
            deps: a,
            exports: null
        }
    }
    
    s.loadDepsJs(id)

    }
複製代碼

因此sea.js,是寫了一個正則的函數,去查詢define中傳入的fn的字符串,而後獲得的依賴數組。。。 而require.js的依賴數組,是我們本身寫而且傳入的:define(['2.js'])。。。

這個正則方法,你們不用去探究,練習時直接用就好了

8. 最後

簡單源碼地址:

my-require.js

my-sea.js

這個文章內容寫得確實有點多有點羅嗦,仍是但願可以講的通俗易懂一點。以後會不斷的完善文章,也歡迎你們留言提問,不對的地方也歡迎指正~~~但願對你們能有幫助

轉載請註明出處,謝謝~~

相關文章
相關標籤/搜索