Seajs源码解析系列(二)


前言:上一篇对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.getModule.prototype.passModule.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
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值