深入理解requireJS-實(shí)現(xiàn)一個簡單的模塊加載器
在前文中我們不止一次強(qiáng)調(diào)過模塊化編程的重要性,以及其可以解決的問題:
① 解決單文件變量命名沖突問題
② 解決前端多人協(xié)作問題
③ 解決文件依賴問題
④ 按需加載(這個說法其實(shí)很假了)
⑤ ......
為了深入了解加載器,中間閱讀過一點(diǎn)requireJS的源碼,但對于很多同學(xué)來說,對加載器的實(shí)現(xiàn)依舊不太清楚
事實(shí)上不通過代碼實(shí)現(xiàn),單單憑閱讀想理解一個庫或者框架只能達(dá)到一知半解的地步,所以今天便來實(shí)現(xiàn)一個簡單的加載器
加載器原理分析
分與合
事實(shí)上,一個程序運(yùn)行需要完整的模塊,以下代碼為例:
//求得績效系數(shù) var performanceCoefficient = function () { return 0.2; }; //住房公積金計算方式 var companyReserve = function (salary) { return salary * 0.2; }; //個人所得稅 var incomeTax = function (salary) { return salary * 0.2; }; //基本工資 var salary = 1000; //最終工資 var mySalary = salary + salary * performanceCoefficient(); mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); console.log(mySalary);
我一份完整的工資來說,公司會有績效獎勵,但是其算法可能非常復(fù)雜,其中可能涉及到出勤率,完成度什么的,這里暫時不管
而有增便有減,所以我們會交住房公積金,也會扣除個人所得稅,最終才是我的工資
對于完整的程序來說上面的流程缺一不可,但是各個函數(shù)中卻有可能異常的復(fù)雜,跟錢有關(guān)系的東西都復(fù)雜,所以單單是公司績效便有可能超過1000行代碼
于是我們這邊便會開始分:
<script src="companyReserve.js" type="text/javascript"></script> <script src="incomeTax.js" type="text/javascript"></script> <script src="performanceCoefficient.js" type="text/javascript"></script> <script type="text/javascript"> //基本工資 var salary = 1000; //最終工資 var mySalary = salary + salary * performanceCoefficient(); mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); console.log(mySalary); </script>
上面的代碼表明上是“分”開了,事實(shí)上也造成了“合”的問題,我要如何才能很好的把它們重新合到一起呢,畢竟其中的文件可能還涉及到依賴,這里便進(jìn)入我們的require與define
require與define
事實(shí)上,上面的方案仍然是以文件劃分,而不是以模塊劃分的,若是文件名發(fā)生變化,頁面會涉及到改變,其實(shí)這里應(yīng)該有一個路徑的映射處理這個問題
var pathCfg = { 'companyReserve': 'companyReserve', 'incomeTax': 'incomeTax', 'performanceCoefficient': 'performanceCoefficient' };
于是我們一個模塊便對應(yīng)了一個路徑j(luò)s文件,剩下的便是將之對應(yīng)模塊的加載了,因為前端模塊涉及到請求。所以這種寫法:
companyReserve = requile('companyReserve');
對于前端來說是不適用的,就算你在哪里看到這樣做了,也一定是其中做了一些“手腳”,這里我們便需要依據(jù)AMD規(guī)范了:
require.config({ 'companyReserve': 'companyReserve', 'incomeTax': 'incomeTax', 'performanceCoefficient': 'performanceCoefficient' }); require(['companyReserve', 'incomeTax', 'performanceCoefficient'], function (companyReserve, incomeTax, performanceCoefficient) { //基本工資 var salary = 1000; //最終工資 var mySalary = salary + salary * performanceCoefficient(); mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary)); console.log(mySalary); });
這里便是一個標(biāo)準(zhǔn)的requireJS的寫法了,首先定義模塊以及其路徑映射,其中定義依賴項
require(depArr, callback)
一個簡單完整的模塊加載器基本就是這個樣子了,首先是一個依賴的數(shù)組,其次是一個回調(diào),回調(diào)要求依賴項全部加載才能運(yùn)行,并且回調(diào)的參數(shù)便是依賴項執(zhí)行的結(jié)果,所以一般要求define模塊具有一個返回值
方案有了,那么如何實(shí)現(xiàn)呢?
實(shí)現(xiàn)方案
說到模塊加載,人們第一反應(yīng)都是ajax,因為無論何時,能拿到模塊文件的內(nèi)容,都是模塊化的基本,但是采用ajax的方式是不行的,因為ajax有跨域的問題
而模塊化方案又不可避免的要處理跨域的問題,所以使用動態(tài)創(chuàng)建script標(biāo)簽加載js文件便成為了首選,但是,不使用ajax的方案,對于實(shí)現(xiàn)難度來說還是有要求
PS:我們實(shí)際工作中還會有加載html模板文件的場景,這個稍候再說
通常我們是這樣做的,require作為程序入口,調(diào)度javascript資源,而加載到各個define模塊后,各個模塊便悄無聲息的創(chuàng)建script標(biāo)簽加載
加載結(jié)束后便往require模塊隊列報告自己加載結(jié)束了,當(dāng)require中多有依賴模塊皆加載結(jié)束時,便執(zhí)行其回調(diào)
原理大致如此,剩下的只是具體實(shí)現(xiàn),而后在論證這個理論是否靠譜即可
加載器閹割實(shí)現(xiàn)
核心模塊
根據(jù)以上理論,我們由整體來說,首先以入口三個基本函數(shù)來說
var require = function () { }; require.config = function () { }; require.define = function () { };
這三個模塊比不可少:
① config用以配置模塊與路徑的映射,或者還有其他用處
② require為程序入口
③ define設(shè)計各個模塊,響應(yīng)require的調(diào)度
然后我們這里會有一個創(chuàng)建script標(biāo)簽的方法,并且會監(jiān)聽其onLoad事件
④ loadScript
其次我們加載script標(biāo)簽后,應(yīng)該有一個全局的模塊對象,用于存儲已經(jīng)加載好的模塊,于是這里提出了兩個需求:
⑤ require.moduleObj 模塊存儲對象
⑥ Module,模塊的構(gòu)造函數(shù)
有了以上核心模塊,我們形成了如下代碼:
(function () { var Module = function () { this.status = 'loading'; //只具有l(wèi)oading與loaded兩個狀態(tài) this.depCount = 0; //模塊依賴項 this.value = null; //define函數(shù)回調(diào)執(zhí)行的返回 }; var loadScript = function (url, callback) { }; var config = function () { }; var require = function (deps, callback) { }; require.config = function (cfg) { }; var define = function (deps, callback) { }; })();
于是接下來便是具體實(shí)現(xiàn),然后在實(shí)現(xiàn)過程中補(bǔ)足不具備的接口與細(xì)節(jié),往往在最后的實(shí)現(xiàn)與最初的設(shè)計沒有半毛錢關(guān)系......
代碼實(shí)現(xiàn)
這塊最初實(shí)現(xiàn)時,本來想直接參考requireJS的實(shí)現(xiàn),但是我們老大笑瞇瞇的拿出了一個他寫的加載器,我一看不得不承認(rèn)有點(diǎn)妖
于是這里便借鑒了其實(shí)現(xiàn),做了簡單改造:
(function () { //存儲已經(jīng)加載好的模塊 var moduleCache = {}; var require = function (deps, callback) { var params = []; var depCount = 0; var i, len, isEmpty = false, modName; //獲取當(dāng)前正在執(zhí)行的js代碼段,這個在onLoad事件之前執(zhí)行 modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN'; //簡單實(shí)現(xiàn),這里未做參數(shù)檢查,只考慮數(shù)組的情況 if (deps.length) { for (i = 0, len = deps.length; i < len; i++) { (function (i) { //依賴加一 depCount++; //這塊回調(diào)很關(guān)鍵 loadMod(deps[i], function (param) { params[i] = param; depCount--; if (depCount == 0) { saveModule(modName, params, callback); } }); })(i); } } else { isEmpty = true; } if (isEmpty) { setTimeout(function () { saveModule(modName, null, callback); }, 0); } }; //考慮最簡單邏輯即可 var _getPathUrl = function (modName) { var url = modName; //不嚴(yán)謹(jǐn) if (url.indexOf('.js') == -1) url = url + '.js'; return url; }; //模塊加載 var loadMod = function (modName, callback) { var url = _getPathUrl(modName), fs, mod; //如果該模塊已經(jīng)被加載 if (moduleCache[modName]) { mod = moduleCache[modName]; if (mod.status == 'loaded') { setTimeout(callback(this.params), 0); } else { //如果未到加載狀態(tài)直接往onLoad插入值,在依賴項加載好后會解除依賴 mod.onload.push(callback); } } else { /* 這里重點(diǎn)說一下Module對象 status代表模塊狀態(tài) onLoad事實(shí)上對應(yīng)requireJS的事件回調(diào),該模塊被引用多少次變化執(zhí)行多少次回調(diào),通知被依賴項解除依賴 */ mod = moduleCache[modName] = { modName: modName, status: 'loading', export: null, onload: [callback] }; _script = document.createElement('script'); _script.id = modName; _script.type = 'text/javascript'; _script.charset = 'utf-8'; _script.async = true; _script.src = url; //這段代碼在這個場景中意義不大,注釋了 // _script.onload = function (e) {}; fs = document.getElementsByTagName('script')[0]; fs.parentNode.insertBefore(_script, fs); } }; var saveModule = function (modName, params, callback) { var mod, fn; if (moduleCache.hasOwnProperty(modName)) { mod = moduleCache[modName]; mod.status = 'loaded'; //輸出項 mod.export = callback ? callback(params) : null; //解除父類依賴,這里事實(shí)上使用事件監(jiān)聽較好 while (fn = mod.onload.shift()) { fn(mod.export); } } else { callback && callback.apply(window, params); } }; window.require = require; window.define = require; })();
首先這段代碼有一些問題:
沒有處理參數(shù)問題,字符串之類皆未處理
未處理循環(huán)依賴問題
未處理CMD寫法
未處理html模板加載相關(guān)
未處理參數(shù)配置,baseUrl什么都沒有搞
基于此想實(shí)現(xiàn)打包文件也不可能
......
但就是這100行代碼,便是加載器的核心,代碼很短,對各位理解加載器很有幫助,里面有兩點(diǎn)需要注意:
① requireJS是使用事件監(jiān)聽處理本身依賴,這里直接將之放到了onLoad數(shù)組中了
② 這里有一個很有意思的東西
document.currentScript
這個可以獲取當(dāng)前執(zhí)行的代碼段
requireJS是在onLoad中處理各個模塊的,這里就用了一個不一樣的實(shí)現(xiàn),每個js文件加載后,都會執(zhí)行require(define)方法
執(zhí)行后便取到當(dāng)前正在執(zhí)行的文件,并且取到文件名加載之,正因為如此,連script的onLoad事件都省了......
demo實(shí)現(xiàn)
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> </body> <script src="require.js" type="text/javascript"></script> <script type="text/javascript"> require(['util', 'math', 'num'], function (util, math, num) { num = math.getRadom() + '_' + num; num = util.formatNum(num); console.log(num); }); </script> </html>
//util define([], function () { return { formatNum: function (n) { if (n < 10) return '0' + n; return n; } }; });
//math define(['num'], function (num) { return { getRadom: function () { return parseInt(Math.random() * num); } }; });
//math define(['num'], function (num) { return { getRadom: function () { return parseInt(Math.random() * num); } }; });
小結(jié)
今天我們實(shí)現(xiàn)了一個簡單的模塊加載器,通過他希望可以幫助各位了解requireJS或者seaJS,最后順利進(jìn)入模塊化編程的行列
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JavaScript實(shí)現(xiàn)開關(guān)等效果
本文給大家分享一段簡單的代碼基于js實(shí)現(xiàn)開關(guān)燈效果,代碼簡單易懂,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2017-09-09僅IE9/10同時支持script元素的onload和onreadystatechange事件分析
測試結(jié)果可以看出,IE9后已經(jīng)開始支持script的onload事件了。一直以來我們判斷js文件是否已經(jīng)加載完成就是用以上的兩個事件。2011-04-04基于JavaScript實(shí)現(xiàn)高德地圖和百度地圖提取行政區(qū)邊界經(jīng)緯度坐標(biāo)
本文給大家介紹javascript實(shí)現(xiàn)高德地圖和百度地圖提取行政區(qū)邊界經(jīng)緯度坐標(biāo)的相關(guān)知識,本文實(shí)用性非常高,代碼簡單易懂,需要的朋友參考下吧2016-01-01學(xué)習(xí)javascript面向?qū)ο?理解javascript對象
這篇文章主要介紹了javascript對象,學(xué)習(xí)javascript面向?qū)ο?,感興趣的小伙伴們可以參考一下2016-01-01Javascript? Constructor構(gòu)造器模式與Module模塊模式
這篇文章主要介紹了Javascript? Constructor構(gòu)造器模式與Module模塊模式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08JS 將偽數(shù)組轉(zhuǎn)換成數(shù)組的實(shí)現(xiàn)示例
這篇文章主要介紹了JS 將偽數(shù)組轉(zhuǎn)換成數(shù)組,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07