在前文中咱們不止一次強調過模塊化編程的重要性,以及其能夠解決的問題:javascript
① 解決單文件變量命名衝突問題html
② 解決前端多人協做問題前端
③ 解決文件依賴問題java
④ 按需加載(這個說法其實很假了)ajax
⑤ ......算法
爲了深刻了解加載器,中間閱讀過一點requireJS的源碼,但對於不少同窗來講,對加載器的實現依舊不太清楚編程
事實上不經過代碼實現,單單憑閱讀想理解一個庫或者框架只能達到只知其一;不知其二的地步,因此今天便來實現一個簡單的加載器跨域
事實上,一個程序運行須要完整的模塊,如下代碼爲例:數組
1 //求得績效係數 2 var performanceCoefficient = function () { 3 return 0.2; 4 }; 5 6 //住房公積金計算方式 7 var companyReserve = function (salary) { 8 return salary * 0.2; 9 }; 10 11 //我的所得稅 12 var incomeTax = function (salary) { 13 return salary * 0.2; 14 }; 15 16 //基本工資 17 var salary = 1000; 18 19 //最終工資 20 var mySalary = salary + salary * performanceCoefficient(); 21 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); 22 console.log(mySalary);
我一份完整的工資來講,公司會有績效獎勵,可是其算法可能很是複雜,其中可能涉及到出勤率,完成度什麼的,這裏暫時無論app
而有增便有減,因此咱們會交住房公積金,也會扣除我的所得稅,最終纔是個人工資
對於完整的程序來講上面的流程缺一不可,可是各個函數中卻有可能異常的複雜,跟錢有關係的東西都複雜,因此單單是公司績效便有可能超過1000行代碼
因而咱們這邊便會開始分:
1 <script src="companyReserve.js" type="text/javascript"></script> 2 <script src="incomeTax.js" type="text/javascript"></script> 3 <script src="performanceCoefficient.js" type="text/javascript"></script> 4 <script type="text/javascript"> 5 6 //基本工資 7 var salary = 1000; 8 9 //最終工資 10 var mySalary = salary + salary * performanceCoefficient(); 11 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); 12 console.log(mySalary); 13 14 </script>
上面的代碼代表上是「分」開了,事實上也形成了「合」的問題,我要如何才能很好的把它們從新合到一塊兒呢,畢竟其中的文件可能還涉及到依賴,這裏便進入咱們的require與define
事實上,上面的方案仍然是以文件劃分,而不是以模塊劃分的,如果文件名發生變化,頁面會涉及到改變,其實這裏應該有一個路徑的映射處理這個問題
var pathCfg = { 'companyReserve': 'companyReserve', 'incomeTax': 'incomeTax', 'performanceCoefficient': 'performanceCoefficient' };
因而咱們一個模塊便對應了一個路徑js文件,剩下的即是將之對應模塊的加載了,由於前端模塊涉及到請求。因此這種寫法:
companyReserve = requile('companyReserve');
對於前端來講是不適用的,就算你在哪裏看到這樣作了,也必定是其中作了一些「手腳」,這裏咱們便須要依據AMD規範了:
1 require.config({ 2 'companyReserve': 'companyReserve', 3 'incomeTax': 'incomeTax', 4 'performanceCoefficient': 'performanceCoefficient' 5 }); 6 7 require(['companyReserve', 'incomeTax', 'performanceCoefficient'], function (companyReserve, incomeTax, performanceCoefficient) { 8 //基本工資 9 var salary = 1000; 10 11 //最終工資 12 var mySalary = salary + salary * performanceCoefficient(); 13 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); 14 console.log(mySalary); 15 });
這裏即是一個標準的requireJS的寫法了,首先定義模塊以及其路徑映射,其中定義依賴項
require(depArr, callback)
一個簡單完整的模塊加載器基本就是這個樣子了,首先是一個依賴的數組,其次是一個回調,回調要求依賴項所有加載才能運行,而且回調的參數即是依賴項執行的結果,因此通常要求define模塊具備一個返回值
方案有了,那麼如何實現呢?
說到模塊加載,人們第一反應都是ajax,由於不管什麼時候,能拿到模塊文件的內容,都是模塊化的基本,可是採用ajax的方式是不行的,由於ajax有跨域的問題
而模塊化方案又不可避免的要處理跨域的問題,因此使用動態建立script標籤加載js文件便成爲了首選,可是,不使用ajax的方案,對於實現難度來講仍是有要求
PS:咱們實際工做中還會有加載html模板文件的場景,這個稍候再說
一般咱們是這樣作的,require做爲程序入口,調度javascript資源,而加載到各個define模塊後,各個模塊便悄無聲息的建立script標籤加載
加載結束後便往require模塊隊列報告本身加載結束了,當require中多有依賴模塊皆加載結束時,便執行其回調
原理大體如此,剩下的只是具體實現,然後在論證這個理論是否靠譜便可
根據以上理論,咱們由總體來講,首先以入口三個基本函數來講
var require = function () { }; require.config = function () { }; require.define = function () { };
這三個模塊比不可少:
① config用以配置模塊與路徑的映射,或者還有其餘用處
② require爲程序入口
③ define設計各個模塊,響應require的調度
而後咱們這裏會有一個建立script標籤的方法,而且會監聽其onLoad事件
④ loadScript
其次咱們加載script標籤後,應該有一個全局的模塊對象,用於存儲已經加載好的模塊,因而這裏提出了兩個需求:
⑤ require.moduleObj 模塊存儲對象
⑥ Module,模塊的構造函數
有了以上核心模塊,咱們造成了以下代碼:
1 (function () { 2 3 var Module = function () { 4 this.status = 'loading'; //只具備loading與loaded兩個狀態 5 this.depCount = 0; //模塊依賴項 6 this.value = null; //define函數回調執行的返回 7 }; 8 9 10 var loadScript = function (url, callback) { 11 12 }; 13 14 var config = function () { 15 16 }; 17 18 var require = function (deps, callback) { 19 20 }; 21 22 require.config = function (cfg) { 23 24 }; 25 26 var define = function (deps, callback) { 27 28 }; 29 30 })();
因而接下來即是具體實現,而後在實現過程當中補足不具有的接口與細節,每每在最後的實現與最初的設計沒有半毛錢關係......
這塊最初實現時,原本想直接參考requireJS的實現,可是咱們老大笑眯眯的拿出了一個他寫的加載器,我一看不得不認可有點妖
因而這裏便借鑑了其實現,作了簡單改造:
1 (function () { 2 3 //存儲已經加載好的模塊 4 var moduleCache = {}; 5 6 var require = function (deps, callback) { 7 var params = []; 8 var depCount = 0; 9 var i, len, isEmpty = false, modName; 10 11 //獲取當前正在執行的js代碼段,這個在onLoad事件以前執行 12 modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN'; 13 14 //簡單實現,這裏未作參數檢查,只考慮數組的狀況 15 if (deps.length) { 16 for (i = 0, len = deps.length; i < len; i++) { 17 (function (i) { 18 //依賴加一 19 depCount++; 20 //這塊回調很關鍵 21 loadMod(deps[i], function (param) { 22 params[i] = param; 23 depCount--; 24 if (depCount == 0) { 25 saveModule(modName, params, callback); 26 } 27 }); 28 })(i); 29 } 30 } else { 31 isEmpty = true; 32 } 33 34 if (isEmpty) { 35 setTimeout(function () { 36 saveModule(modName, null, callback); 37 }, 0); 38 } 39 40 }; 41 42 //考慮最簡單邏輯便可 43 var _getPathUrl = function (modName) { 44 var url = modName; 45 //不嚴謹 46 if (url.indexOf('.js') == -1) url = url + '.js'; 47 return url; 48 }; 49 50 //模塊加載 51 var loadMod = function (modName, callback) { 52 var url = _getPathUrl(modName), fs, mod; 53 54 //若是該模塊已經被加載 55 if (moduleCache[modName]) { 56 mod = moduleCache[modName]; 57 if (mod.status == 'loaded') { 58 setTimeout(callback(this.params), 0); 59 } else { 60 //若是未到加載狀態直接往onLoad插入值,在依賴項加載好後會解除依賴 61 mod.onload.push(callback); 62 } 63 } else { 64 65 /* 66 這裏重點說一下Module對象 67 status表明模塊狀態 68 onLoad事實上對應requireJS的事件回調,該模塊被引用多少次變化執行多少次回調,通知被依賴項解除依賴 69 */ 70 mod = moduleCache[modName] = { 71 modName: modName, 72 status: 'loading', 73 export: null, 74 onload: [callback] 75 }; 76 77 _script = document.createElement('script'); 78 _script.id = modName; 79 _script.type = 'text/javascript'; 80 _script.charset = 'utf-8'; 81 _script.async = true; 82 _script.src = url; 83 84 //這段代碼在這個場景中意義不大,註釋了 85 // _script.onload = function (e) {}; 86 87 fs = document.getElementsByTagName('script')[0]; 88 fs.parentNode.insertBefore(_script, fs); 89 90 } 91 }; 92 93 var saveModule = function (modName, params, callback) { 94 var mod, fn; 95 96 if (moduleCache.hasOwnProperty(modName)) { 97 mod = moduleCache[modName]; 98 mod.status = 'loaded'; 99 //輸出項 100 mod.export = callback ? callback(params) : null; 101 102 //解除父類依賴,這裏事實上使用事件監聽較好 103 while (fn = mod.onload.shift()) { 104 fn(mod.export); 105 } 106 } else { 107 callback && callback.apply(window, params); 108 } 109 }; 110 111 window.require = require; 112 window.define = require; 113 114 })();
首先這段代碼有一些問題:
沒有處理參數問題,字符串之類皆未處理
未處理循環依賴問題
未處理CMD寫法
未處理html模板加載相關
未處理參數配置,baseUrl什麼都沒有搞
基於此想實現打包文件也不可能
......
但就是這100行代碼,即是加載器的核心,代碼很短,對各位理解加載器頗有幫助,裏面有兩點須要注意:
① requireJS是使用事件監聽處理自己依賴,這裏直接將之放到了onLoad數組中了
② 這裏有一個頗有意思的東西
document.currentScript
這個能夠獲取當前執行的代碼段
requireJS是在onLoad中處理各個模塊的,這裏就用了一個不同的實現,每一個js文件加載後,都會執行require(define)方法
執行後便取到當前正在執行的文件,而且取到文件名加載之,正由於如此,連script的onLoad事件都省了......
1 <html xmlns="http://www.w3.org/1999/xhtml"> 2 <head> 3 <title></title> 4 </head> 5 <body> 6 </body> 7 <script src="require.js" type="text/javascript"></script> 8 <script type="text/javascript"> 9 require(['util', 'math', 'num'], function (util, math, num) { 10 11 num = math.getRadom() + '_' + num; 12 num = util.formatNum(num); 13 console.log(num); 14 }); 15 </script> 16 </html>
1 //util 2 define([], function () { 3 return { 4 formatNum: function (n) { 5 if (n < 10) return '0' + n; 6 return n; 7 } 8 }; 9 });
1 //math 2 define(['num'], function (num) { 3 return { 4 getRadom: function () { 5 return parseInt(Math.random() * num); 6 } 7 }; 8 });
1 //num 2 define([], function () { 3 return 10; 4 });
今天咱們實現了一個簡單的模塊加載器,經過他但願能夠幫助各位瞭解requireJS或者seaJS,最後順利進入模塊化編程的行列