前言:上一篇对Seajs及其使用做了简单的介绍,这一章开始正式接触Seajs的源码。
Sea.js 的所有代码都通过 GitHub 管理,项目地址:https://github.com/seajs/seajs
其源码放在src目录下。
目录结构:
scr目录结构:
-------------------------
intro.js -- 全局闭包头部
sea.js -- 基本命名空间
util-lang.js -- 语言增强
util-events.js -- 简易事件机制
util-path.js -- 路径处理
util-request.js -- HTTP 请求
util-deps.js -- 依赖提取
module.js -- 核心代码
config.js -- 配置
outro.js -- 全局闭包尾部
本章主要对module.js模块进行介绍。
代码分析:
module.js作为整个Seajs的核心,无疑是十分重要的。Seajs作为一个模块加载器,其所有的模块都会作为一个Module对象存储在seajs.cache
中。通过seajs.cache
,我们可以查阅当前模块系统中的所有模块信息。
一个模块在加载过程中必然会经历以下几个阶段:
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1, //开始加载当前模块
// 2 - The meta data has been saved to cachedMods
SAVED: 2, //当前模块加载完成并保存模块数据
// 3 - The `module.dependencies` are being loaded
LOADING: 3, //开始加载依赖的模块
// 4 - The module are ready to execute
LOADED: 4, //依赖模块已经加载完成
// 5 - The module is being executed
EXECUTING: 5, //当前模块执行中
// 6 - The `module.exports` is available
EXECUTED: 6, //当前模块执行完毕
// 7 - 404
ERROR: 7 //出错
}
同时,我们在内存中使用Module
对象来维护模块的信息:
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []//依赖模块ID列表
this.deps = {} // Ref the dependence modules
this.status = 0
this._entry = []//在模块加载完成之后需要调用callback的模块
}
对应模块加载的几个阶段状态,总结模块加载的大致过程如下:
模块方法 | 方法说明 | 状态变化 |
---|---|---|
seajs.use | 在页面上启动模块系统,加载一个或多个模块 | |
Module.use: | 开始加载整个流程 | 状态变化:FETCHING到SAVED |
Module.prototype.load | 开始加载子模块 | 状态变化:SAVED到LOADING |
Module.prototype.onload | 子模块加载完成时调用 | 状态变化:LOADING到LOADED |
Module.prototype.exec | 加载过程结束,开始执行模块 | 状态变化:EXECUTING到EXECUTED |
接下来,我们来具体的看一下Seajs模块加载的具体过程:
1.首先,我们来看一下seajs.use
,这是整个模块加载的启动器。
/**
* 用来在页面中加载一个或多个模块
* @param ids 模块名 (文件路径)
* @param callback 回调函数
*/
seajs.use = function(ids, callback) {
Module.use(ids, callback, data.cwd + "_use_" + cid())
return seajs
}
示例如下:
seajs.use("./main",function(main){
main.init();
})
我们可以传入想要加载的模块名以及回调函数,进行模块的加载。
2.接下来,我们来看一下Module.use
方法
// Use function is equal to load a anonymous module load相当于执行一个匿名module
/**
*开始整个加载流程,状态初始化为FETCHING到SAVED
* @param ids 依赖模块
* @param callback 模块id
* @param uri 获取uri模块
*/
Module.use = function (ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
mod._entry.push(mod)
mod.history = {}
mod.remain = 1 //未加载的模块个数
mod.callback = function() {
var exports = []
var uris = mod.resolve()
for (var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec()
}
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
delete mod.history
delete mod.remain
delete mod._entry
}
mod.load()
}
在这里,mod._entry
表示在模块加载完成之后需要调用callback的模块。
在当前 Module
对象加载完毕之后,调用load方法。
3. Module.prototype.load
方法中包含了seajs
模块加载的整体逻辑。用来加载当前模块的依赖模块,并在全部模块加载完毕之后触发onload方法。
// Load module.dependencies and fire onload when all done
Module.prototype.load = function() {
var mod = this
// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}
mod.status = STATUS.LOADING
// Emit `load` event for plugins such as combo plugin
var uris = mod.resolve()
emit("load", uris)
for (var i = 0, len = uris.length; i < len; i++) {
mod.deps[mod.dependencies[i]] = Module.get(uris[i])//获取所有的依赖模块
}
// Pass entry to it's dependencies
//将依赖uri遍历,判断是否全部加载
mod.pass()
// If module has entries not be passed, call onload
if (mod._entry.length) {
mod.onload()
return
}
// Begin parallel loading 开始并行加载
var requestCache = {}
var m
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache) //从服务端递归拉取依赖模块,并执行load
}
else if (m.status === STATUS.SAVED) {
m.load()//若依赖模块已经被获取,直接执行load
}
}
// Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]()
}
}
}
在这一步中,模块的状态由SAVED变为LOADING,所以当模块的状态大于LOADING时,直接return 等待触发onload即可。
这里,介绍一下Module.get
,Module.prototype.pass
和Module.prototype.fetch
方法,具体的源码在这里我就不贴出来了,在本章的结尾会统一贴出。各个方法的大体功能如下:
Module.get
:Get an existed module or create a new one, and cache it in the cacheMods.(源码注释解释的很好很浅显,我就不翻译了(^__^) );Module.prototype.pass
:遍历依赖uri,判断是否全部加载;Module.prototype.fetch
:fetch a module。遍历当前模块所依赖的模块,把依赖模块从服务端加载出来,加载完成后,浏览器会立即执行模块的define方法,此时依赖模块的状态为saved。
4.当模块全部加载完毕之后,执行Module.prototype.onload
函数。此时模块的状态由LOADING变为LOADED。entry.remain
表示未加载的模块的个数,当其===0时,执行module的回调函数,并删除mod._entry,释放内存。
// Call this method when module is loaded
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
// When sometimes cached in IE, exec will occur before onload, make sure len is an number
for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
var entry = mod._entry[i]
//当依赖模块全部完成的时候,执行callback函数
if (--entry.remain === 0) {
entry.callback()
}
}
delete mod._entry
}
5.在回调函数中,通过exports
暴露模块及其依赖模块的接口,并通过apply方法,将exports作为参数,执行global.callback();最后删除无用的变量,释放内存。
mod.callback = function() {
var exports = []
var uris = mod.resolve()
for (var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec()
}
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
delete mod.history
delete mod.remain
delete mod._entry
}
到这里,Seajs的模块加载核心逻辑也就基本上说完了。身为一个刚接触Seajs的菜鸟,难免有介绍不完善甚至有误的地方,小伙伴们阅读之后,可以给我留言或者私信提出意见,欢迎交流。(^__^)
下面附上Module.js的源码以及阅读时添加的注释,比较长,有兴趣的朋友可以看一下。
/**
* module.js - The core of module loader
*/
var cachedMods = seajs.cache = {}
var anonymousMeta
var fetchingList = {}
var fetchedList = {}
var callbackList = {}
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1, //开始加载当前模块
// 2 - The meta data has been saved to cachedMods
SAVED: 2, //当前模块加载完成并保存模块数据
// 3 - The `module.dependencies` are being loaded
LOADING: 3, //开始加载依赖的模块
// 4 - The module are ready to execute
LOADED: 4, //依赖模块已经加载完成
// 5 - The module is being executed
EXECUTING: 5, //当前模块执行中
// 6 - The `module.exports` is available
EXECUTED: 6, //当前模块执行完毕
// 7 - 404
ERROR: 7 //出错
}
//内存中用Module对象来维护模块的信息
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []//依赖模块ID列表
this.deps = {} // Ref the dependence modules
this.status = 0
this._entry = []//在模块加载完成之后需要调用callback的模块
}
// Resolve module.dependencies
//获取mod的所有dep的uri。
Module.prototype.resolve = function() {
var mod = this
var ids = mod.dependencies
var uris = []
for (var i = 0, len = ids.length; i < len; i++) {
uris[i] = Module.resolve(ids[i], mod.uri)
}
return uris
}
//遍历依赖uri,判断是否全部加载,如果全部加载的话,最后mod._entry.length的值为0
Module.prototype.pass = function() {
var mod = this
//依赖模块个数
var len = mod.dependencies.length
for (var i = 0; i < mod._entry.length; i++) {
var entry = mod._entry[i]
var count = 0
//遍历依赖模块
for (var j = 0; j < len; j++) {
var m = mod.deps[mod.dependencies[j]]//依赖的模块
// If the module is unload and unused in the entry, pass entry to it
if (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) {
entry.history[m.uri] = true
count++
m._entry.push(entry)
//递归遍历依赖
if(m.status === STATUS.LOADING) {
m.pass()
}
}
}
// If has passed the entry to it's dependencies, modify the entry's count and del it in the module
if (count > 0) {
entry.remain += count - 1
mod._entry.shift()
i--
}
}
}
// Load module.dependencies and fire onload when all done
Module.prototype.load = function() {
var mod = this
// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}
mod.status = STATUS.LOADING
// Emit `load` event for plugins such as combo plugin
var uris = mod.resolve()
emit("load", uris)
for (var i = 0, len = uris.length; i < len; i++) {
mod.deps[mod.dependencies[i]] = Module.get(uris[i])//获取所有的依赖模块
}
// Pass entry to it's dependencies
//将依赖uri遍历,判断是否全部加载
mod.pass()
// If module has entries not be passed, call onload
if (mod._entry.length) {
mod.onload()
return
}
// Begin parallel loading 开始并行加载
var requestCache = {}
var m
for (i = 0; i < len; i++) {
m = cachedMods[uris[i]]
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache) //从服务端递归拉取依赖模块,并执行load
}
else if (m.status === STATUS.SAVED) {
m.load()//若依赖模块已经被获取,直接执行load
}
}
// Send all requests at last to avoid cache bug in IE6-9. Issues#808
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
requestCache[requestUri]()
}
}
}
// Call this method when module is loaded
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
// When sometimes cached in IE, exec will occur before onload, make sure len is an number
for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
var entry = mod._entry[i]
//当依赖模块全部完成的时候,执行callback函数
if (--entry.remain === 0) {
entry.callback()
}
}
delete mod._entry
}
// Call this method when module is 404
Module.prototype.error = function() {
var mod = this
mod.onload()
mod.status = STATUS.ERROR
}
// Execute a module
Module.prototype.exec = function () {
var mod = this
// When module is executed, DO NOT execute it again. When module
// is being executed, just return `module.exports` too, for avoiding
// circularly calling
if (mod.status >= STATUS.EXECUTING) {
return mod.exports
}
mod.status = STATUS.EXECUTING
if (mod._entry && !mod._entry.length) {
delete mod._entry
}
//non-cmd module has no property factory and exports
if (!mod.hasOwnProperty('factory')) {
mod.non = true
return
}
// Create require
var uri = mod.uri
function require(id) {
var m = mod.deps[id] || Module.get(require.resolve(id))
if (m.status == STATUS.ERROR) {
throw new Error('module was broken: ' + m.uri)
}
return m.exec()
}
require.resolve = function(id) {
return Module.resolve(id, uri)
}
require.async = function(ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid())
return require
}
// Exec factory
var factory = mod.factory
var exports = isFunction(factory) ?
factory(require, mod.exports = {}, mod) :
factory
if (exports === undefined) {
exports = mod.exports
}
// Reduce memory leak
delete mod.factory
mod.exports = exports
mod.status = STATUS.EXECUTED
// Emit `exec` event
emit("exec", mod)
return mod.exports
}
// Fetch a module 取得模块
//遍历当前模块所依赖的模块,把依赖模块从服务端加载出来,加载完成后,浏览器会立即执行模块的define方法,此时依赖模块的状态为saved。
Module.prototype.fetch = function(requestCache) {
var mod = this
var uri = mod.uri
mod.status = STATUS.FETCHING
// Emit `fetch` event for plugins such as combo plugin
var emitData = { uri: uri }
emit("fetch", emitData)
var requestUri = emitData.requestUri || uri
// Empty uri or a non-CMD module
if (!requestUri || fetchedList.hasOwnProperty(requestUri)) {
mod.load()
return
}
if (fetchingList.hasOwnProperty(requestUri)) {
callbackList[requestUri].push(mod)
return
}
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
// Emit `request` event for plugins such as text plugin
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: isFunction(data.charset) ? data.charset(requestUri) : data.charset,
crossorigin: isFunction(data.crossorigin) ? data.crossorigin(requestUri) : data.crossorigin
})
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
//从服务端请求模块
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset, emitData.crossorigin)
}
function onRequest(error) {
delete fetchingList[requestUri]
fetchedList[requestUri] = true//取得的清单
// Save meta data of anonymous module 保存匿名模块的元数据
if (anonymousMeta) {
Module.save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) {
// When 404 occurs, the params error will be true
if(error === true) {
m.error()
}
else {
m.load()
}
}
}
}
// Resolve id to uri
//处理依赖模块的url,把依赖模块的id转换为url并返回。(获取模块的全路径)
Module.resolve = function(id, refUri) {
// Emit `resolve` event for plugins such as text plugin
var emitData = { id: id, refUri: refUri }
emit("resolve", emitData)
return emitData.uri || seajs.resolve(emitData.id, refUri)
}
// Define a module
Module.define = function (id, deps, factory) {
var argsLen = arguments.length
// define(factory)
if (argsLen === 1) {
factory = id
id = undefined
}
else if (argsLen === 2) {
factory = deps
// define(deps, factory)
if (isArray(id)) {
deps = id
id = undefined
}
// define(id, factory)
else {
deps = undefined
}
}
// Parse dependencies according to the module factory code
//找出代码中声明的依赖ID
if (!isArray(deps) && isFunction(factory)) {
deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString())//获取代码中声明的依赖关系
}
var meta = {
id: id,
uri: Module.resolve(id),//id2uri function
deps: deps,
factory: factory
}
// Try to derive uri in IE6-9 for anonymous modules
if (!isWebWorker && !meta.uri && doc.attachEvent && typeof getCurrentScript !== "undefined") {
var script = getCurrentScript()
if (script) {
meta.uri = script.src
}
// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri
}
// Emit `define` event, used in nocache plugin, seajs node version etc
emit("define", meta)
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
// Save meta data to cachedMods
Module.save = function(uri, meta) {
var mod = Module.get(uri)
// Do NOT override already saved modules
if (mod.status < STATUS.SAVED) {
mod.id = meta.id || uri
mod.dependencies = meta.deps || []
mod.factory = meta.factory
mod.status = STATUS.SAVED
emit("save", mod)
}
}
// Get an existed module or create a new one, and cache it in the cacheMods.
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}
// Use function is equal to load a anonymous module load相当于执行一个匿名module
/**
*开始整个加载流程,状态初始化为FETCHING到SAVED
* @param ids 依赖模块
* @param callback 模块id
* @param uri 获取uri模块
*/
Module.use = function (ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
mod._entry.push(mod)
mod.history = {}
mod.remain = 1 //未加载的模块个数
mod.callback = function() {
var exports = []
var uris = mod.resolve()
for (var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec()
}
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
delete mod.history
delete mod.remain
delete mod._entry
}
mod.load()
}
// Public API
//在页面上启动模块系统
/**
* 用来在页面中加载一个或多个模块
* @param ids 模块名 (文件路径)
* @param callback 回调函数
*/
seajs.use = function(ids, callback) {
Module.use(ids, callback, data.cwd + "_use_" + cid())
return seajs
}
Module.define.cmd = {}
global.define = Module.define
// For Developers
seajs.Module = Module
data.fetchedList = fetchedList
data.cid = cid
seajs.require = function(id) {
var mod = Module.get(Module.resolve(id))
if (mod.status < STATUS.EXECUTING) {
mod.onload()
mod.exec()
}
return mod.exports
}