seajs源码分析(一)整体结构以及module.js

1,seajs的主要内容在module.js内部,最开始包含这么几个东西

var cachedMods = seajs.cache = {}
var anonymousMeta

var fetchingList = {}
var fetchedList = {}
var callbackList = {}

还有几个状态:

var STATUS = Module.STATUS = {
  // 1 - `module.uri` 获取中
  FETCHING: 1,
  // 2 - 模块已经存储在了cachedMods对象中
  SAVED: 2,
  // 3 - 模块依赖加载中
  LOADING: 3,
  // 4 - 模块已经加载完成,准备执行
  LOADED: 4,
  // 5 - 模块执行中
  EXECUTING: 5,
  // 6 - 模块已执行完成
  EXECUTED: 6
}

2,页面中主要部分是一个Module类

function Module(uri, deps) {
  this.uri = uri
  this.dependencies = deps || []
  this.exports = null
  this.status = 0

  // 依赖于本模块的模块
  this._waitings = {}

  // 本模块未加载完的依赖数
  this._remain = 0
}

3,大体结构如上,具体实现的话得先从seajs的入口seajs.use说起:

//可以看到,ids实际上是在preload的模块全部开始加载后才开始加载的
seajs.use = function(ids, callback) {
  Module.preload(function() {
    Module.use(ids, callback, data.cwd + "_use_" + cid())
  })
  return seajs
}

seajs是个什么对象?

var seajs = global.seajs = {
  // The current version of Sea.js being used
  version: "@VERSION"
}

global的传入是这样的:

(function(global, undefined) {

// 在seajs多次加载时避免冲突
if (global.seajs) {
  return
}

})(this);

跟jquery很像,只是传入的是this,应该是为了兼容node等其他环境,在浏览器内,global就是window

4,module.preload

Module.preload = function(callback) {
  var preloadMods = data.preload
  var len = preloadMods.length

  if (len) {
    Module.use(preloadMods, function() {
      // 移除已经加载好的预加载模块
      preloadMods.splice(0, len)

      // 允许预加载模块再次定义预加载模块(没用过……)
      Module.preload(callback)
    }, data.cwd + "_preload_" + cid())
  }
  else {
    callback()
  }
}

实际上它的过程是一直不断的读取需要预加载的内容,最后调用callback。

preload的内容,是从data.preload读取来的,在config.js文件内部:

data.preload = (function() {
  var plugins = []

  // 这段代码获取url?后面的字段,并把"seajs-xxx"转换成"seajs-xxx=1"
  // 注意: 在uri或cookie内使用"seajs-xxx=1"标志来预加载"seajx-xxx"文件
var str = loc.search.replace(/(seajs-\w+)(&|$)/g, "$1=1$2") // 在字符串后面连接上cookie这一大段字符串 str += " " + doc.cookie // 将"seajs-xxx=1"的"seajs-xxx"存入plugins数组中,并返回
str.replace(/(seajs-\w+)=1/g, function(m, name) { plugins.push(name) }) return plugins })()

这段代码是preload的默认字段,另外里面有几个变量:

var data = seajs.data = {}
var doc = document
var loc = location
var cwd = dirname(loc.href)
var scripts = doc.getElementsByTagName("script")

preload还有一部分是我们自己配置的,在config.js文件内,还有一段这样的代码:

seajs.config = function(configData) {

  //configData就是我们的所有配置,遍历之
  for (var key in configData) {
    var curr = configData[key]//存储当前key的配置信息
    var prev = data[key]

    // 如果之前有调用过seajs.config函数进行配置,那么把这些配置信息内的对象合并起来
    if (prev && isObject(prev)) {
      for (var k in curr) {
        prev[k] = curr[k]
      }
    }
    else {
      //如果是数组的话,就concat起来
      if (isArray(prev)) {
        curr = prev.concat(curr)
      }
      //确认base是个绝对路径
      //如果base不是以"/"结尾,加上"/",addBase的作用是将相对路径转换成绝对路径
      else if (key === "base") {
        (curr.slice(-1) === "/") || (curr += "/")
        curr = addBase(curr)
      }

      data[key] = curr
    }
  }
  //emit是触发事件的函数
  emit("config", configData)
  return seajs
}

 至此,data这个对象是什么,config存哪了,就都清楚了,另外,其实在config.js内还有这样一个默认值:

//这段正则用来匹配path/seajs/xxx/xx/xx/...的path/部分然后存到data.base中
var BASE_RE = /^(.+?\/)(\?\?)?(seajs\/)+/

data.base = (loaderDir.match(BASE_RE) || ["", loaderDir])[1]
data.dir = loaderDir
data.cwd = cwd
data.charset = "utf-8"

 我们看到,module.preload调用了module.use,那module.use是个什么东西呢?

5,Module.use

Module.use = function (ids, callback, uri) {
  var mod = Module.get(uri, isArray(ids) ? ids : [ids])

  //这个绑定在mod上的callback会在onload后触发,作用是获取各个依赖模块的输出,并且调用传入的callback,这个callback起真正作用的实际上是从seajs.use中传入的那个函数(如果有的话),因为Module.preload调用Module.use时传入的callback效果实际上在不断的调用自身,直到没有需要预加载的模块。实际效果是seajs.use(['a', 'b', 'c'], function(a, b, c){})
  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
  }

  mod.load()
}

 这里使用了一大堆未知方法,Module.get, mod.load, mod.exec, mod.resolve

6,Module.get

//如果存在则获取该Module对象,否则以模块url为id,存入cachedMods这个对象中,实际上所有新建的Module对象都存在这个对象中
Module.get = function(uri, deps) {
  return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}

7,Module.prototype.resolve(mod.resolve)

//该方法获取所有依赖的完成id,然后返回,调用了Module.resolve方法
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
}

可以看到,该方法依赖于Module.resolve方法

8,Module.resolve

Module.resolve = function(id, refUri) {
  // 为插件触发resolve事件比如text插件
  var emitData = { id: id, refUri: refUri }
  emit("resolve", emitData)

  //id2Uri函数将id(比如相对路径)转换成完整的路径,作为存储id
  return emitData.uri || id2Uri(emitData.id, refUri)
}

9,Module.prototype.load(mod.load)

这个方法是重头戏,而且有点长

Module.prototype.load = function() {
  var mod = this

  // 如果已经加载或正在被加载,不要重复加载
  if (mod.status >= STATUS.LOADING) {
    return
  }

  mod.status = STATUS.LOADING

  // 为插件触发load事件,比如combo插件,获取依赖,存到uris变量中
  var uris = mod.resolve()
  emit("load", uris)

  //获取依赖数
  var len = mod._remain = uris.length
  var m

  // 依赖模块初始化,并给每个依赖的模块_waiting赋初值
  for (var i = 0; i < len; i++) {
    m = Module.get(uris[i])

    //没加载完的模块,添加_waiting
    if (m.status < STATUS.LOADED) {
      // 有可能重复依赖?
      m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1
    }
    else {
      mod._remain--
    }
  }

  //依赖加载完,则该模块加载完成,触发onload
  if (mod._remain === 0) {
    mod.onload()
    return
  }

  // 开始加载
  var requestCache = {}

  for (i = 0; i < len; i++) {
    //事实上在resolve后在cachedMods对象中都存储了模块id
    m = cachedMods[uris[i]]
    
    //如果自身没加载,则加载该模块,调用了m.fetch,事实上fetch并没有真正执行内容获取,而仅仅是绑定发送请求的方法,requestCache的作用,是为下面的for循环做准备,具体看fetch。
    if (m.status < STATUS.FETCHING) {
      m.fetch(requestCache)
    }
    else if (m.status === STATUS.SAVED) {
      m.load()
    }
  }

  // 这里才是真正执行该方法,这是为了解决 IE6-9的缓存bug , 具体请看github上的Issues#808
  for (var requestUri in requestCache) {
    if (requestCache.hasOwnProperty(requestUri)) {
      requestCache[requestUri]()
    }
  }
}

该方法内部调用了mod.fetch,mod.onload方法,除此之外,还有一个mod.exec方法在前面被Module.use依赖

10,Module.prototype.fetch(mod.fetch)

Module.prototype.fetch = function(requestCache) {
  var mod = this
  var uri = mod.uri

  mod.status = STATUS.FETCHING

  // 为combo等插件触发fetch事件
  var emitData = { uri: uri }
  emit("fetch", emitData)
  var requestUri = emitData.requestUri || uri

  // 如果uri不符合规范或者已经加载好,直接加载依赖
  if (!requestUri || fetchedList[requestUri]) {
    mod.load()
    return
  }

  //如果使用了combo插件,且在加载队列中有该页面的uri,只需要将该模块推入callback中,等待触发onload就是了
  if (fetchingList[requestUri]) {
    callbackList[requestUri].push(mod)
    return
  }
  //否则,推入fetchingList,并初始化该uri的callbackList
  fetchingList[requestUri] = true
  callbackList[requestUri] = [mod]

  // 为text等插件触发request事件
  emit("request", emitData = {
    uri: uri,
    requestUri: requestUri,
    onRequest: onRequest,
    charset: data.charset
  })
  
  //如果还没有请求,且传入了requestCache对象,则将uri和发送函数对应推入该对象中,如果没有传入,直接启动发送
  if (!emitData.requested) {
    requestCache ?
        requestCache[emitData.requestUri] = sendRequest :
        sendRequest()
  }
//此处调用了request方法,作用是将uri作为一个link或者script标签          //的链接,onrequest函数是onload的回调函数,启动发送后将script标签删除
  function sendRequest() {
    request(emitData.requestUri, emitData.onRequest, emitData.charset)
  }

  function onRequest() {
   //完成后将该模块在fetching列表中删除,推入fetched列表
    delete fetchingList[requestUri]
    fetchedList[requestUri] = true

    // 此时模块运行,实际会有一个调用Module.define的过程,如果该模块是匿名模块,define会给anonymousMeta赋值
    //如果是匿名模块,则调用Module.save
    if (anonymousMeta) {
      Module.save(uri, anonymousMeta)
      anonymousMeta = null
    }

    // 该requestUri的所有模块接收完成,调用其load方法
    var m, mods = callbackList[requestUri]
    delete callbackList[requestUri]
    while ((m = mods.shift())) m.load()
  }
}

该方法依赖于Module.save方法,实际上隐式依赖于Module.define方法

11,Module.define

Module.define = function (id, deps, factory) {
  var argsLen = arguments.length

  //这一长串ifelse逻辑是为了解析传入的参数
  // 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
    }
  }

  // 如果没有申明依赖,并且传入了函数,则调用parseDependencies来解析该函数内容
  if (!isArray(deps) && isFunction(factory)) {
    deps = parseDependencies(factory.toString())
  }

  //此处尝试给模块命名id,如果没有成功,则生成匿名模块,等待在后期触发onrequest的时候在将其使用Module.save命名
  var meta = {
    id: id,
    uri: Module.resolve(id),
    deps: deps,
    factory: factory
  }

  // 这个是尝试在ie6-9下面给uri复制,getCurrentScript为获取当前的script节点
  if (!meta.uri && doc.attachEvent) {
    var script = getCurrentScript()

    if (script) {
      meta.uri = script.src
    }

  }

  // 为nocache插件触发define事件
  emit("define", meta)

  //如果存在uri,则存储,否则,定义为匿名模块
  meta.uri ? Module.save(meta.uri, meta) :
      anonymousMeta = meta
}

 这里,实际上如果在定义模块的时候没有使用数组存入依赖,很有可能会成为一个匿名模块,但是依赖它的模块会在require里面申明它的一个uri,并在load的时候调用Module.get使用该uri为该匿名模块创建了一个Module对象,所以在启动onrequest之前,若有匿名模块,则肯定是拥有该onrequest函数的模块,然后会在函数内部给该匿名模块命名。此处使用了Module.save。

12,Module.save

Module.save = function(uri, meta) {
  var mod = Module.get(uri)

  // 确认模块没被存储,如果是匿名模块,则meta.id是没有的
  if (mod.status < STATUS.SAVED) {
    mod.id = meta.id || uri
    mod.dependencies = meta.deps || []
    mod.factory = meta.factory
    mod.status = STATUS.SAVED
  }
}

13,Module.prototype.onload(mod.onload)

//只有在所有依赖加载完后,才会调用onload
Module.prototype.onload = function() {
  var mod = this
  mod.status = STATUS.LOADED

 //这不就是需要调用module.use内绑定的callback么
  if (mod.callback) {
    mod.callback()
  }

  //检测依赖该模块的模块,_remain相应减少,如果为0,则调用onload
  var waitings = mod._waitings
  var uri, m

  for (uri in waitings) {
    if (waitings.hasOwnProperty(uri)) {
      m = cachedMods[uri]
      m._remain -= waitings[uri]
      if (m._remain === 0) {
        m.onload()
      }
    }
  }

  // 释放内存
  delete mod._waitings
  delete mod._remain
}

14,Module.prototype.exec(mod.exec)

这个放到最后,是因为一个模块在所有工作做完后,才会调用该方法

Module.prototype.exec = function () {
  var mod = this

  // 避免重复计算
  if (mod.status >= STATUS.EXECUTING) {
    return mod.exports
  }

  mod.status = STATUS.EXECUTING

  // 这里创建了最常用的require函数
  var uri = mod.uri

  //返回依赖模块的计算值exports
  function require(id) {
    return Module.get(require.resolve(id)).exec()
  }

  require.resolve = function(id) {
    return Module.resolve(id, uri)
  }
  //异步加载,触发callback
  require.async = function(ids, callback) {
    Module.use(ids, callback, uri + "_async_" + cid())
    return require
  }

  var factory = mod.factory

  var exports = isFunction(factory) ?
      factory(require, mod.exports = {}, mod) :
      factory
  //有可能在factory是通过给exports复制来传递结果的
  if (exports === undefined) {
    exports = mod.exports
  }

  // 出错,触发error事件(没有结果或者该模块是CSS文件)
  if (exports === null && !IS_CSS_RE.test(uri)) {
    emit("error", mod)
  }

  // 释放内存
  delete mod.factory

  mod.exports = exports
  mod.status = STATUS.EXECUTED

  // 触发excec事件
  emit("exec", mod)
  //最终计算出了该模块的返回值
  return exports
}
原文地址:https://www.cnblogs.com/strayling/p/3488692.html