Requirejs是AMD規(guī)范的比較好的實(shí)踐,在看Requirejs之前先看下AMD的規(guī)范
AMD規(guī)范
看的是github上的中文翻譯的規(guī)范。
就是很簡單的一個(gè)define方法
define(id?, dependencies?, factory);
- id為這個(gè)定義的模塊的名字,如果不提供,默認(rèn)為指定的腳本的名字,如果提供了,必須是唯一的
- 二參為依賴,就是模塊需要先執(zhí)行的模塊。如果不提供或者提供默認(rèn)的,就默認(rèn)是["require", "exports", "module"],就會掃描工廠方法的require來獲得依賴。就是CommonJS的寫法了(這個(gè)不就是CMD嗎?)
- 三參為函數(shù),在所有的依賴被加載后會自動執(zhí)行
任何全局函數(shù)必須有一個(gè)amd屬性來標(biāo)識遵循AMD編程接口。
Requirejs
這個(gè)加載器主要解決的問題:
- 大量JS引入頁面的時(shí)候會導(dǎo)致頁面假死,用了之后就變成異步的了
- 還有處理各模塊之間的依賴,因?yàn)椴挥玫脑捒赡躂S的引入順序需要自己控制好
Requirejs使用
就是引一下requirejs,然后data-main指明入口文件。就像這樣:
<script src="require.js" data-main="main.js"></script>
然后我們在入口文件里面就可以申明依賴以及函數(shù)了。
//main.js
//引了一個(gè)模塊sec
requirejs(["sec"],function(sec){
sec.action()
});
//sec.js
//這里按照commonjs的形式讓requirejs自己去搜一遍,再引入third.js
define(function (require, exports, module) {
var third =require('third');
third();
module.exports.action = function () {console.log(22)};
});
//third.js
define(function (require, exports, module) {
module.exports = function () {console.log(11)};
});
入口文件里寫requirejs或者define其實(shí)都行的。只是requirejs顯示是在入口文件。這一這里的sec.js等等都是異步加載的,不會導(dǎo)致頁面死在那里。
文件的路徑問題
我們可以在主入口js中使用requirejs.config來進(jìn)行配置。
requirejs.config({
baseUrl: "js/lib",
paths: {
"sec": "sec.min"
}
})
我們可以配置基本的url,可以針對特別的模塊單獨(dú)定義路徑,這里的路徑還可以是網(wǎng)絡(luò)請求的位置。
加載非amd規(guī)范的的模塊
我們引入的模塊都必須是define申明好的,如果不是的話,得在require里面申明一下。exports為對外輸出的變量
require.config({
shim: {
'underscore':{
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require.js的插件
提供了domready這種類似的插件
require(['domready!'], function (doc){
// called once the DOM is ready
});
寫一個(gè)require
在閱讀源碼之前,先試著實(shí)現(xiàn)一把感覺這樣子看源碼的實(shí)現(xiàn)會學(xué)到更多。鏈接
先實(shí)現(xiàn)data-main的主入口,很簡單就是分析整個(gè)頁面,得到data-main,然后新建了一個(gè)script,來異步加載js。
var scripts = document.getElementsByTagName('script');
var sLength = scripts.length;
var mainJs;
for(var i=0;i<sLength;i++){
var name = scripts[i].getAttribute("data-main");
if(name){
mainJs = name;
}
}
var mainScript = document.createElement('script');
mainScript.src = mainJs;
document.body.appendChild(mainScript);
然后就是聲明define方法,我最開始的時(shí)候使用的是onload來通知,這樣子可以解決一層的依賴。就是如果main依賴了a和b,是可以的。但是如果b依賴了c,這種方法就失敗了。
//allModule用來保存所有加載的模塊
var allModule = {};
//主要的define方法
var define = function(id,array,cb){
var length = array.length;
if(length > 0){
var i = 0;
array.forEach(function(value,index,array){
var tempScript = document.createElement('script');
tempScript.src = array[index]+'.js';
//目前用的onload來監(jiān)聽加載完畢,但是如果有依賴的話,就不行了
tempScript.onload = function(){
i++;
finish();
}
document.body.appendChild(tempScript);
})
function finish(){
if(i == length){
var modules =[];
for(var x = 0 ; x<array.length;x++){
modules[x] = allModule[array[x]];
}
allModule[id] = cb.apply(null ,modules);
}
}
}else{
allModule[id] = cb();
}
};
于是我不用onload來通知,選擇在不依賴其他的模塊調(diào)用完成時(shí)來進(jìn)行通知,并且引用過他的模塊嘗試執(zhí)行callback,如果這個(gè)模塊所需要的都加載完了就可以執(zhí)行。一層層的向上通知。
//allModule用來保存所有加載的模塊
var allModule = {};
//主要的define方法
function _registerModule(id,dependence,father){
if(!allModule[id]){
allModule[id]= {};
var newModule = allModule[id];
newModule.func = undefined;//模塊的結(jié)果
newModule.dependence = dependence;//依賴的模塊
newModule.dependenceLoadNum = 0;//已經(jīng)依賴的模塊
newModule.finishLoad = function(){};//完成load之后觸發(fā)的方法
if(father){
if(newModule.referrer){
newModule.referrer.push(father);
}else{
newModule.referrer = [];//被引用到的模塊
newModule.referrer.push(father);
}
}
}else{
var newModule = allModule[id];
if(father){
newModule.referrer.push(father);
}
if(dependence){
newModule.dependence = dependence;
newModule.dependenceLoadNum = 0;
}
}
}
var define = function(id,array,cb){
_registerModule(id,array,'',cb);
array.forEach(function(value,index,array){
_registerModule(array[index],[],id,cb);
});
var thisModule = allModule[id];
if(array.length > 0){
array.forEach(function(value,index,array){
var tempScript = document.createElement('script');
tempScript.src = array[index]+'.js';
document.body.appendChild(tempScript);
})
thisModule.finishLoad = _finish;
}else{
thisModule.func = cb();
_refererFinish();
}
function _finish(){
thisModule.dependenceLoadNum++;
if(thisModule.dependenceLoadNum == thisModule.dependence.length){
var modules =[];
for(var x = 0 ; x < array.length; x++){
modules[x] = allModule[array[x]].func;
}
thisModule.func = cb.apply(null ,modules);
}
_refererFinish();
}
function _refererFinish(){
if(thisModule.referrer){
thisModule.referrer.forEach(function(value,index,array){
allModule[array[index]].finishLoad();
});
}
}
};
初次優(yōu)化
在勐喆的指導(dǎo)下進(jìn)行了一次簡單的邏輯優(yōu)化,因?yàn)樵诂F(xiàn)在的實(shí)現(xiàn)中,每個(gè)模塊都既存了依賴的列表,以及被依賴到的列表??梢酝ㄟ^事件監(jiān)聽的形式進(jìn)行一次解耦。也就是模塊不再需要知道誰依賴了他,通過注冊事件的形式來通知。
//allModule用來保存所有加載的模塊
var allModule = [];
function Module(id,dependence){
this.func = undefined;
this.dependence = dependence;
this.dependenceLoadNum = 0;
this.handlers = {};
}
Module.prototype={
on: function(name,handler){
if(!this.handlers[name]){
this.handlers[name] = [];
this.handlers[name].push(handler);
}else{
this.handlers[name].push(handler);
}
},
emit:function(name){
if(!!this.handlers[name]){
this.handlers[name].forEach(function(value){
value();
})
}
}
}
function _registerModule(id,dependence){
var i = allModule.length;
if(!allModule[id]){
allModule[i++] = allModule[id]= new Module(id,dependence);
}else{
if(dependence){
allModule[id].dependence = dependence;
allModule[id].dependenceLoadNum = 0;
}
}
dependence.forEach(function(value){
_registerModule(value,[]);
});
}
//cb為加載完了執(zhí)行的方法
var define = function(id,array,cb){
//注冊相關(guān)的模塊
_registerModule(id,array);
var thisModule = allModule[id];
if(array.length > 0){
array.forEach(function(value){
thisModule.on('finish' + value,function(){
_finish();
_notifyModule();
});
var tempScript = document.createElement('script');
tempScript.src = value+'.js';
document.body.appendChild(tempScript);
})
}else{
thisModule.func = cb();
_notifyModule();
}
function _finish(){
thisModule.dependenceLoadNum++;
if(thisModule.dependenceLoadNum == thisModule.dependence.length){
var modules =[];
array.forEach(function(value,index){
modules[index] = allModule[value].func;
})
thisModule.func = cb.apply(null ,modules);
}
}
//通知全局的模塊加載完畢
function _notifyModule(){
allModule.forEach(function(value,index,array){
array[index].emit('finish' + id);
});
}
};
這樣子的話,define模塊的時(shí)候就會注冊依賴模塊的監(jiān)聽器。然后在依賴的模塊執(zhí)行完了時(shí)候就會進(jìn)行全局的觸發(fā)_notifyModule,通知每一個(gè)模塊這個(gè)模塊加載OK。于是注冊過這個(gè)事件的模塊就會觸發(fā)_finish方法。這樣子最大的好處是進(jìn)行了一次解耦,代價(jià)是會進(jìn)行全局的廣播的形式來通知。其實(shí)我們?nèi)绻胮romise來做的話,可以更好的管理狀態(tài),但是為了兼容性沒有去嘗試。
再次優(yōu)化
上面全局通知的代價(jià)有些高,看完源碼之后才發(fā)現(xiàn),其實(shí)事件可以綁定在dependence的模塊上面,這樣就不用全局通知了。而且這次的修改支持多個(gè)文件同時(shí)引用一個(gè)模塊了。
function _registerModule(id,dependence,defined){
var i = allModule.length;
if(!allModule[id]){
allModule[i++] = allModule[id]= new Module(id,dependence);
allModule[id].defined = defined;
}else{
if(dependence){
allModule[id].dependence = dependence;
allModule[id].dependenceLoadNum = 0;
}
}
dependence.forEach(function(value){
_registerModule(value,[],false);
});
}
//cb為加載完了執(zhí)行的方法
var define = function(id,array,cb){
//注冊相關(guān)的模塊
_registerModule(id,array,true);
var thisModule = allModule[id];
if(array.length > 0){
array.forEach(function(value){
allModule[value].on('finish',function(){
_finish();
})
if(!(allModule[value].defined)){
var tempScript = document.createElement('script');
tempScript.src = value+'.js';
document.body.appendChild(tempScript);
}
})
}else{
thisModule.func = cb();
thisModule.emit('finish');
}
function _finish(){
thisModule.dependenceLoadNum++;
if(thisModule.dependenceLoadNum == thisModule.dependence.length){
var modules =[];
array.forEach(function(value,index){
modules[index] = allModule[value].func;
})
thisModule.func = cb.apply(null ,modules);
//繼續(xù)向上層觸發(fā)
thisModule.emit('finish');
}
}
};
這次的優(yōu)化改了define方法,去掉了全局的通知。還增加了defined屬性來支持多個(gè)模塊引用同一個(gè)模塊。
順便給個(gè)github的傳送門,喜歡的朋友star一下啊,自己平時(shí)遇到的問題以及一下學(xué)習(xí)的思考都會在上面記錄~
參考: