RequireJS最簡實現
網上有不少解析RequireJS源碼的文章,我覺得意義不大。閱讀源代碼的目的不是為了熟悉代碼,而是為了學習核心實現原理。相對RequireJS的源碼, kitty.js 的實現更簡單,更容易理解。本文正是抄了kitty.js的實現,是一個更精簡的RequireJS,用于理解RequireJS的實現原理。
github dummy-requirejs 。這個實現僅支持核心feature:
require(deps, callback) // deps 是依賴數組
define(id, deps, factory) // factory是一個函數
例子參考git中rect.js/main.js。
從實現來看,require/define是基本一致的,require的callback等同于define的factory:都會要求deps被加載且被執行,獲得deps的exports作為整個module傳入callback/factory。不同的是,factory的返回值會被作為define出來的模塊的export,可被視為模塊本身;而callback返回值被忽略。
從用法來看,define僅是定義了模塊,這個模塊可能被作為deps被其他模塊依賴,但define傳入的factory此時是不執行的;而require則會觸發各個模塊的factory執行。
實現
主要實現分為3部分內容,其中關鍵的地方在于模塊載入。
數據結構
既然是模塊加載器,并且需要處理模塊之間的依賴問題,所以設置一個哈希表保存所有的模塊。
var mods = {} // <id, Module>
function Module(id) {
var mod = this
mod.id = id
mod.uri = id // 簡單起見,根據id拼出uri: abc.js
mod.deps = [] // 依賴的模塊id列表
mod.factory = blank // 定義模塊時的factory
mod.callback = blank // 模塊加載完畢后回調
mod.exports = {} // 模塊導出的對象
}
define的實現就比較簡單,主要就是往mods里添加一個Module對象,簡單來說就是:
function define(id, deps, factory) {
var mod = getModule(id) // mods存在就返回,否則就往mods里新增
mod.deps = deps
mod.factory = factory
}
模塊載入
遇到require時就會產生模塊載入的動作。模塊載入時可能發生以下動作:
- 往頁面添加script標簽以讓瀏覽器從服務端拉取js文件
- js文件中可能遇到define從而立即添加模塊 (非AMD模塊不考慮)
- define定義的模塊可能有其他依賴模塊,遞歸載入這些模塊,直到所有模塊載入完畢
這里的模塊載入只是把模塊js文件載入到瀏覽器環境中。以上過程對應的大概代碼為:
Module.prototype.load = function() {
var mod = this
if (mod.status == STATUS.FETCHING) return
if (mod.status == STATUS.UNFETCH) {
return mod.fetch() // 添加script標簽從服務端拉取文件
}
mod.status = STATUS.LOADING
mod.remain = mod.deps.length // 所有依賴載入完畢后通知回調
function callback() {
mod.remain--
if (mod.remain === 0) {
mod.onload() // 通知回調
}
}
each(mod.deps, function (dep) {
var m = getModule(dep) // 獲取依賴模塊對象,依賴模塊可能已經被載入也可能沒有
if (m.status >= STATUS.LOADED || m.status == STATUS.LOADING) { // 已經載入
mod.remain--
return
}
m.listeners.push(callback)
if (m.status < STATUS.LOADING) {
m.load()
}
})
if (mod.remain == 0) {
mod.onload()
}
}
load的實現由于混合了異步問題,所以理解起來會有點難。fetch的實現就是一般的往頁面添加script及設置回調的過程。在fetch完畢后會重新調用load以完成遞歸載入該模塊的依賴:
// 該函數回調時,該js文件已經被瀏覽器執行,其內容包含define則會添加模塊(當然已經被添加過了)
// 可以回頭看上面的define調用的是getModule,此時會重新設置deps/factory等屬性
function onloadListener() {
var readyState = script.readyState;
if (typeof readyState === 'undefined' || /^(loaded|complete)$/.test(readyState)) {
mod.status = STATUS.FETCHED
mod.load()
}
}
模塊生效
模塊載入后模塊其實還沒生效,還無法使用模塊中定義的各種符號。要讓模塊生效,就得執行模塊定義的factory函數。在直接間接依賴的模塊被全部載入完成后,最終回調到我們的callback。此時可以看看require的實現:
// 前面提到require/define實現類似,所以這里創建了Module對象,只是復用代碼
function require(deps, callback) {
var mod = new Module(getId())
mod.deps = deps
mod.factory = callback
mod.callback = function () {
mod.exec()
}
mod.status = STATUS.FETCHED
mod.load()
}
就是簡單地調用了load,完成后調用了exec。exec又是一個涉及到遞歸的函數,它會遞歸執行所有模塊的factory。factory的執行需要各個模塊的exports對象,只有模塊exec后才會得到exports對象。
Module.prototype.exec = function() {
var mod = this
if (mod.status >= STATUS.EXECUTED) { return mod.exports }
// 獲取依賴模塊的exports列表
var args = mod.getDepsExport()
var ret = mod.factory.apply(null, args)
// factory 返回值作為該模塊的exports
mod.exports = ret
mod.status = STATUS.EXECUTED
return mod.exports
}
上面的代碼主要是實現這樣的功能:
// 將依賴[d1, d2]的exports作為參數d1,d2傳入
define('my-module', ['d1', 'd2'], function (d1, d2) {
return {func: function() {}}
})
getDepsExport就是一個取依賴模塊exports的過程:
Module.prototype.getDepsExport = function() {
var mod = this
var exports = []
var deps = mod.deps
var argsLen = mod.factory.length < deps.length ? mod.factory.length : deps.length
for (var i = 0; i < argsLen; i++) {
exports.push(mod.require(deps[i]))
}
return exports
}
Module.require(id)用于exec目標模塊并返回其exports:
Module.prototype.require = function(dep) {
// 由于之前已經遞歸載入過所有模塊,所以該依賴模塊必然是已經存在的,可以被exec的
var mod = getModule(dep)
return mod.exec()
}
于是又回到了exec,實現了遞歸執行所有依賴模塊的功能。exec主要是獲取依賴模塊exports并調用factory,所以最初的require將用戶的callback作為factory傳入那個臨時Module,最終使得調用到用戶的callback。
通過以上過程,實際上就已經走通了從define到require實現的整個過程。整個代碼不到200行。基于此可以添加更多RequireJS的附加功能。完。
來自:http://www.udpwork.com/item/16088.html