如今初入前端的同窗們,都直接就上手webpack了,而在幾年前沒有node仍是jquery打天下的時候,不借助node或軟件讓不一樣js文件之間互相引用、模塊化開發,是件很麻煩的事。javascript
接下來會介紹兩個有名的工具AMD(require.js)和CMD(sea.js),雖然如今用的不多了,但仍是須要知道模塊化經歷的過程,面試也可能會問到。html
總之,上面的結構,在前端內容愈來愈多,尤爲ajax的趨勢、先後端分離、愈來愈注重前端體驗,js文件愈來愈多且互相引用更復雜的狀況下,真心亂套了,因此須要有一個新的模塊化工具。前端
咱們固然是但願像如今這樣,文件之間互相 import、export 就好了,但遺憾的是,這是es6配合node的用法,須要服務端作支撐處理文件,而一開始僅經過靜態文件去模塊化。java
即Asynchronous Module Definition,中文名是異步模塊定義的意思。它是一個模塊化開發的規範,是一種思路,是一個概念。咱們不用過於糾結它究竟是個啥,只須要知道它是一個規範概念,而大名鼎鼎的require.js,是它的一個具體的實現,之前不少都用這個工具開發。node
這裏只介紹大概簡單的用法,沒用過的同窗最好去看教程。jquery
login.html中webpack
引入了require.js文件,而後指定了入口文件login.jses6
~~web
入口文件login.js中面試
loginModule.js中
loginCtrl.js中
注意:
實際上就是定義了兩個全局變量函數,一個require(),一個define()
其實這兩個函數功能和原理差很少,你能夠認爲他倆除了名字不同,其餘都差很少。
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文件的順序,都和之前沒啥區別!!!
到這裏咱們大概知道了,這兩個工具的意義,是讓開發者再也不須要寫一堆 <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 !!!
!!!重要方法:
// 建一個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!!!
文件之間有依賴關係的,因此加載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 事件,不斷的遞歸嵌套的加載依賴文件。事實上,最麻煩的也是這裏處理依賴文件,尤爲是一個文件可能被多個文件所依賴,的狀況。
這一點必定要理解,這也是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
用法例子
// 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進行遞歸那裏,很難去講清楚,須要本身去寫去實踐,這裏也很難全都描述清楚。。。
結尾會給一個簡單的能運行的例子
不要想着花一兩個小時就搞定全部了,剛開始確實會看的煩,多回來幾回,隔段時間再研究一下,每次都會加深一點
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要好一點。。。
對依賴模塊的執行時機不一樣,注意:不是加載的時機,模塊加載的時機是同樣的!!!
文件加載順序: 都是先加載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
源碼,大部分和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'])。。。
這個正則方法,你們不用去探究,練習時直接用就好了
簡單源碼地址:
這個文章內容寫得確實有點多有點羅嗦,仍是但願可以講的通俗易懂一點。以後會不斷的完善文章,也歡迎你們留言提問,不對的地方也歡迎指正~~~但願對你們能有幫助
轉載請註明出處,謝謝~~