JavaScript 模塊化編程(一):模塊的寫法
JavaScript 模塊化編程(二):規(guī)范
JavaScript 模塊化編程(三):實現(xiàn)一個RequireJS
JavaScript 模塊化編程(四):結(jié)合Node源碼分析CommonJs規(guī)范
雖然RequireJS是一個過時的JS模塊化解決方案,但是其對我們了解JS模塊化的發(fā)展依然很重要。下面將參考RequireJS源碼寫了一個簡單的JavaScript模塊加載器。只有300行代碼,是源碼的六分之一,已經(jīng)放在我的github上
--------點擊查看代碼--------
1.RequireJS
先回顧一下RequireJS的兩個重要api
定義模塊
define(id?, dependencies?, factory)
加載模塊
require([module], callback)
require和define函數(shù)的關(guān)系
(1)require和define函數(shù)接收同樣的參數(shù),define和require在依賴處理和回調(diào)執(zhí)行上都是一樣的
(2)define的回調(diào)函數(shù)需要有return語句返回模塊對象,這樣define定義的模塊才能被其他模塊引用,所以用它來定義模塊(建議在一個文件中使用一次);require的回調(diào)函數(shù)不需要return語句。其實在我看來,require函數(shù)可以看做是特殊的define函數(shù),它用來定義一個頂層匿名模塊,用來加載和使用模塊,這個模塊不需要被其他模塊加載。
RequireJS中的執(zhí)行流程
(1)RequireJS首先找到data-main屬性,然后根據(jù)屬性值(通過新建一個script標(biāo)簽)加載并且解析入口文件
(2)先調(diào)用 require 函數(shù)和define函數(shù),將所有的依賴轉(zhuǎn)化為script 節(jié)點插入到dom 中,然后每一個 節(jié)點的onload事件中,將該模塊作為實體保存起來,并檢查所有模塊是否加載完成,如果加載完成,遞歸執(zhí)行所有回調(diào)
2.實現(xiàn)一個簡單的RequireJS
首先這里先說明一點,下面我們主要了解實現(xiàn)的整體流程,會粘出一些主要代碼,里面可能會包含一些輔助函數(shù),具體可以查看我github上的源碼
(1)定義一個全局變量
首先,需要定義幾個全局變量,用來保存已經(jīng)加載的模塊,尚未加載的模塊,所有模塊等全局信息
var context={
topModule:"", //頂層模塊
waitings:[], // 尚未加載的模塊
loadeds:[], // 已經(jīng)加載的模塊
baseUrl:"",
/**
* 每一個模塊都有下面的幾個屬性:
* moduleNmae 模塊名稱
* deps 模塊依賴
* factory 模塊工廠函數(shù) .
* args 該模塊的依賴模塊的返回值
* returnValue 該模塊工廠函數(shù)的返回值
*/
modules:[] // 模塊集合
} ;
(2)加載 require 頂層模塊
在require.js 里面都是用 data-main 屬性來指定入口文件,所以先尋找 data-main 屬性,并插入到 head中 。這里將 data-main 作為的路徑作為 baseUrl
/**
* 查找data-main屬性的script標(biāo)簽,
* 根據(jù)屬性值(通過新建一個script標(biāo)簽)加載并且解析入口文件
*/
if (isBrowser) {
var scripts=document.getElementsByTagName('script');
var head,src,subPath,mainScript;
eachList(scripts,function(script){
var dataMain = script.getAttribute('data-main');
if (dataMain) {
if (!head) {
head = script.parentNode;
}
if (!context.baseUrl) {
src = dataMain.split('/');
mainScript = src.pop();
subPath = src.length ? src.join('/') + '/' : './';
context.baseUrl = subPath;
}
// 創(chuàng)建頂層節(jié)點
var dataMainNode = document.createElement('script');
dataMainNode.async = true;
head.appendChild(dataMainNode);
dataMainNode.src = dataMain+ ".js";
dataMainNode.onload = function() {
// 將頂層模塊 從waitings里面除去,并添加到loadeds數(shù)組中
removeByEle(context.waitings, context.topModule)
context.loadeds.push(context.topModule);
}
return true;
}
});
(3)定義 require 方法
require 方法用來使用模塊,也就是定義一個頂層模塊,這個模塊不需要被其他模塊加載
/**
* require方法,加載一個模塊
* @param {[type]} deps [依賴數(shù)組]
* @param {Function} callback [工廠函數(shù)]
* @return {[type]}
*/
requireJs.require=function(deps,callback){
if (typeof name !== 'string') {
callback = deps;
deps = name;
name = null;
}
if (!isArray(deps)) {
callback = deps;
deps = [];
}
// 生成隨機模塊名,方法
let moduleName = getUnqName();
context.topModule = moduleName;
context.waitings.push(moduleName);
// 生成一個模塊配置
context.modules[moduleName] = {
moduleName: moduleName,
deps: deps,
factory: callback,
args: [],
returnValue: ""
}
deps.forEach(function(dep) {
var scriptNode = document.createElement("script");
scriptNode.setAttribute("data-module-name", dep);
scriptNode.async = true;
scriptNode.src = context.baseUrl + dep + ".js";
document.querySelector("head").appendChild(scriptNode);
scriptNode.onload = scriptOnload;
context.waitings.push(dep);
});
}
這里需要注意一個函數(shù)scriptOnload,在script 節(jié)點加載完成后觸發(fā)。將對應(yīng)模塊從waitings 里面刪除,同時往loadeds里面添加該模塊,如果發(fā)現(xiàn) waitings為空,那么就開始遞歸執(zhí)行工廠函數(shù) 。
/**
* [每一個腳本插入head中,都會執(zhí)行這個事件 。這個函數(shù)完成兩件事:
* 1. 如果是一個匿名模塊加載,那么取得這個匿名模塊,并完成模塊命名,
* 2. 當(dāng)節(jié)點加載完畢,判斷context.waitings是否為空,如果不為空,返回,如果為空,說明已經(jīng)全部加載完畢,現(xiàn)在就可以執(zhí)行所有的工廠函數(shù)]
* @param {[object]} e [事件對象]
* @return {[type]}
*/
function scriptOnload(e) {
e = e || window.event;
let node = e.target;
let moduleName = node.getAttribute('data-module-name');
tempModule.moduleName = moduleName;
context.modules[moduleName] = tempModule;
removeByEle(context.waitings, moduleName);
context.loadeds.push(moduleName);
if (!context.waitings.length) {
console.log(context.modules);
exec(context.topModule);
}
}
(4)定義 define 方法
其實define函數(shù)和上面的require函數(shù)做了差不多相同的事,差別在于require自動生成了一個模塊名。并且require中設(shè)置了context.topModule
/**
* [define和 require 做的工作幾乎相同]
* @param {[array]} deps [依賴數(shù)組]
* @param {[function]} callback [工廠函數(shù)]
* @return {[type]}
*/
requireJs.define=function(name,deps,callback){
if (typeof name !== 'string') {
callback = deps;
deps = name;
name = null;
}
if (!isArray(deps)) {
callback = deps;
deps = [];
}
//生成一個模塊配置
tempModule = {
deps: deps,
factory: callback,
args: [],
returnValue: ""
}
// 遞歸遍歷所有依賴,添加到 `head` 中,并設(shè)置 這個節(jié)點的一個屬性`data-module-name`標(biāo)識模塊名
deps.forEach(function(dep) {
var scriptNode = document.createElement("script");
scriptNode.setAttribute("data-module-name", dep);
scriptNode.async = true;
scriptNode.src = context.baseUrl + dep + ".js";
document.querySelector("head").appendChild(scriptNode);
scriptNode.onload = scriptOnload;
context.waitings.push(dep);
});
}
(5)執(zhí)行回調(diào)
我們再回到scriptOnload 函數(shù),每個模塊加載完成,就會在 waitings 里面去掉,然后檢查waitings 數(shù)組,如果為空,說明全部加載完,就可以執(zhí)行 exec函數(shù),在這里函數(shù)中,遞歸執(zhí)行所有的回調(diào) 。
/**
* 所有模塊加載完畢,遞歸執(zhí)行工程函數(shù) , 核心方法
* @param {[string]} moduleName [模塊名]
* @return {[type]}
*/
function exec(moduleName) {
let module = context.modules[moduleName];
let deps = module.deps;
let args = [];
if(deps){
deps.forEach(function(dep) {
exec(dep);
args.push(context.modules[dep].returnValue);
});
module.args = args;
module.returnValue = context.modules[moduleName].factory.apply(context.modules[moduleName], args);
}
}
3.測試
//user.js
define([], function () {
return {
checkLogin: function (name,pwd) {
return name==="xxx"&&pwd==="yyy"
}
}
})
//math.js
define(function () {
return {
add: function (a,b){
return a+b;
},
sub:function(a,b){
return a-b;
}
}
})
//main.js
require(["math","user"], function(math,user) {
if(user.checkLogin("xxx","yyz")){
console.log("12+21=" + math.add(12,21));
}else{
console.log('please sign in or register first');
}
})
//test.html
<script src="../require.js" data-main="main"></script>
結(jié)果
