seajs源码阅读

seajs主要解决的问题包括:命名冲突、文件依赖、异步加载和模块化等问题,具体怎么实现的呢?通过阅读源码一探究竟。源码地址:https://github.com/seajs/seajs

 

seajs-debug.js

/**
 * Sea.js 3.0.0
 */
(function(global, undefined) {

//多次加载seajs.js保证只有一个seajs.js文件有效
  if (global.seajs) {
    return
  }

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

//seajs的配置信息
  var data = seajs.data = {}

  /**
   * 类型判断信息
   */
  function isType(type) {
    return function(obj) {
      return {}.toString.call(obj) == "[object " + type + "]"
    }
  }

  var isObject = isType("Object")
  var isString = isType("String")
  var isArray = Array.isArray || isType("Array")
  var isFunction = isType("Function")

  var _cid = 0
  function cid() {
    return _cid++
  }

  /**
   * util-events.js - The minimal events support
   */
  var events = data.events = {}

  /**
   * 注册事件
   * @param name
   * @param callback
   * @returns {{version: string}}
   */
  seajs.on = function(name, callback) {
    var list = events[name] || (events[name] = [])
    list.push(callback)
    return seajs
  }

// Remove event. If `callback` is undefined, remove all callbacks for the
// event. If `event` and `callback` are both undefined, remove all callbacks
// for all events
  seajs.off = function(name, callback) {
    // Remove *all* events
    //如果name和callback都为空,则清除所有的事件
    if (!(name || callback)) {
      events = data.events = {};
      return seajs
    }

    var list = events[name] ;
    if (list) {
      //如果回调函数为空,则删除所有的时间监听,否则移除指定的监听事件
      if (callback) {
        for (var i = list.length - 1; i >= 0; i--) {
          if (list[i] === callback) {
            list.splice(i, 1)
          }
        }
      }else {
        delete events[name]
      }
    }

    return seajs
  }

// Emit event, firing all bound callbacks. Callbacks receive the same
// arguments as `emit` does, apart from the event name
//触发指定的事件监听
  var emit = seajs.emit = function(name, data) {
    var list = events[name] ;
    if (list) {
      // Copy callback lists to prevent modification
      list = list.slice()
      // Execute event callbacks, use index because it's the faster.
      for(var i = 0, len = list.length; i < len; i++) {
        list[i](data)
      }
    }

    return seajs
  }

  /**
   * util-path.js - The utilities for operating path such as id, uri
   */
//^在方括号中使用,表示不接受字符集合。 只匹配所有的路径信息,最后一个标识符为“/”
  var DIRNAME_RE = /[^?#]*\// ;
//替换/./为/
  var DOT_RE = /\/\.\//g ;
//替换“/路径名/../”为单个“/”
  var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\// ;
//替换多个“//”为一个“/”
  var MULTI_SLASH_RE = /([^:/])\/+\//g ;

  /**
   * 导出请求中的路径,不包括具体的文件名称
   * dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/"
   * ref: http://jsperf.com/regex-vs-split/2
   * @param path
   * @returns {*}
   */
  function dirname(path) {
    return path.match(DIRNAME_RE)[0]
  }

  /**
   * 对路径进行格式化,对路径中特殊转义字符进行了转换,例如:realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c"
   * @param path
   * @returns {XML|string|*}
   */
  function realpath(path) {
    // /a/b/./c/./d ==> /a/b/c/d
    path = path.replace(DOT_RE, "/")
    /*
     @author wh1100717
     a//b/c ==> a/b/c
     a///b/////c ==> a/b/c
     DOUBLE_DOT_RE matches a/b/c//../d path correctly only if replace // with / first
     */
    path = path.replace(MULTI_SLASH_RE, "$1/")

    // a/b/c/../../d  ==>  a/b/../d  ==>  a/d
    while (path.match(DOUBLE_DOT_RE)) {
      path = path.replace(DOUBLE_DOT_RE, "/")
    }

    return path
  }

  /**
   * url后缀名称自动添加“.js”
   * @param path
   * @returns {string}
   */
  function normalize(path) {
    var last = path.length - 1
    var lastC = path.charCodeAt(last)

    if (lastC === 35 /* "#" */) {
      return path.substring(0, last)
    }
    //此处只是针对js文件做了处理。在一般的请求后缀后添加“.js”
    return (path.substring(last - 2) === ".js" ||  path.indexOf("?") > 0 || lastC === 47 /* "/" */) ?
        path : path + ".js"
  }

  //只获取路径中第一个“/”之前的文字
  var PATHS_RE = /^([^/:]+)(\/.+)$/ ;
  //匹配大括号内的内容
  var VARS_RE = /{([^{]+)}/g ;

  /**
   * 检查是否有配置别名,如果未配置别名,则需要使用原有id
   * @param id
   * @returns {*}
   */
  function parseAlias(id) {
    var alias = data.alias ;
    return alias && isString(alias[id]) ? alias[id] : id ;
  }

  /**
   * 根据id解析路径,id的组成部分使用“/”进行分隔
   * @param id
   * @returns {*}
   */
  function parsePaths(id) {
    var paths = data.paths ;
    var m ;

    //paths是一个{}对象,解析id的组成部分,使用path中指定的变量替换路径名称
    //解析路径中第一个“/”之前的字符串是否有指定path,如果有指定path,则用指定的path替换当前路径中的字符串
    if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
      id = paths[m[1]] + m[2]
    }

    return id
  }

  /**
   * 使用正则表达式解析字符串中的变量。变量使用“{}”包含
   * @param id
   * @returns {*}
   */
  function parseVars(id) {
    //通过配置参数中传递
    var vars = data.vars ;
    if (vars && id.indexOf("{") > -1) {
      //function参数,第一个为所有匹配的文本,第二个为指定的子匹配文本
      id = id.replace(VARS_RE, function(m, key) {
        return isString(vars[key]) ? vars[key] : m ;
      })
    }

    return id
  }

  /**
   * 通过map对请求的url进行转换,其中map为一个数组。
   * 如果数组元素为一个函数,则直接调用函数对uri进行转换,否则将当前元素看做为一个数组,使用第二个元素替换第一个元素
   * @param uri
   * @returns {*}
   */
  function parseMap(uri) {
    var map = data.map ;
    var ret = uri ;
    if (map) {
      for (var i = 0, len = map.length; i < len; i++) {
        var rule = map[i] ;

        //针对uri进行映射转换
        ret = isFunction(rule) ?
            (rule(uri) || uri) :
            uri.replace(rule[0], rule[1]) ;
        //只应用一次uri的映射转换
        if (ret !== uri) {
          break ;
        }
      }
    }
    return ret
  }

  //匹配“//.”或“:/”
  var ABSOLUTE_RE = /^\/\/.|:\// ;
  //根目录正则表达式,匹配当前请求的根请求路径,基本规则为:“协议://域名/”。此处使用了正则表达式的非贪婪匹配
  var ROOT_DIR_RE = /^.*?\/\/.*?\// ;
  /**
   * 添加请求路径信息。此处对应seajs中文件路径加载方式
   * @param id
   * @param refUri
   * @returns {XML|string|*}
   */
  function addBase(id, refUri) {
    var ret ;
    var first = id.charCodeAt(0)

    if (ABSOLUTE_RE.test(id)) {
      //如果是绝对路径
      ret = id
    }else if (first === 46 /* "." */) {
      //如果是相对路径,则获取当前请求的地址路径,然后加上当前的模块的id
      ret = (refUri ? dirname(refUri) : data.cwd) + id
    }else if (first === 47 /* "/" */) {
      //如果是根目录开始,则获取请求地址的根目录,然后加上当前资源的id
      var m = data.cwd.match(ROOT_DIR_RE)
      ret = m ? m[0] + id.substring(1) : id
    }else {
      //如果是别名字段等,则默认使用seajs配置的js加载根目录
      ret = data.base + id
    }

    //如果请求地址是以“//”开头,则默认使用当前请求的默认协议
    if (ret.indexOf("//") === 0) {
      ret = location.protocol + ret
    }

    //格式化路径
    return realpath(ret)
  }

  /**
   * 将id解析为请求的url
   * @param id
   * @param refUri
   * @returns {*}
   */
  function id2Uri(id, refUri) {
    if (!id) return "" ;

    //执行顺序为:格式化别名、格式化路径、格式化别名、格式化变量、格式化别名、格式化文件后缀、格式化别名
    id = parseAlias(id) ;
    id = parsePaths(id) ;
    id = parseAlias(id) ;
    id = parseVars(id) ;
    id = parseAlias(id) ;
    id = normalize(id) ;
    id = parseAlias(id) ;

    //添加根路径
    var uri = addBase(id, refUri) ;

    uri = parseAlias(uri) ;
    uri = parseMap(uri) ;

    return uri
  }

  /**
   * 格式化id为路径
   * @type {id2Uri}
   */
  seajs.resolve = id2Uri;

// Check environment
  var isWebWorker = typeof window === 'undefined' && typeof importScripts !== 'undefined' && isFunction(importScripts) ;

//部分浏览器打开的地址为:about:xxx and blob:xxx,忽略这些地址
  var IGNORE_LOCATION_RE = /^(about|blob):/;

  var loaderDir;
// Sea.js's full path
  var loaderPath;

//获取当前的工作目录,在web浏览器使用场景下,就是当前请求的地址,在路径“/”之前的字符
  var cwd = (!location.href || IGNORE_LOCATION_RE.test(location.href)) ? '' : dirname(location.href);

  if (isWebWorker) {
    // Web worker doesn't create DOM object when loading scripts
    // Get sea.js's path by stack trace.
    var stack;
    try {
      var up = new Error();
      throw up;
    } catch (e) {
      // IE won't set Error.stack until thrown
      stack = e.stack.split('\n');
    }
    // First line is 'Error'
    stack.shift();

    var m;
    // Try match `url:row:col` from stack trace line. Known formats:
    // Chrome:  '    at http://localhost:8000/script/sea-worker-debug.js:294:25'
    // FireFox: '@http://localhost:8000/script/sea-worker-debug.js:1082:1'
    // IE11:    '   at Anonymous function (http://localhost:8000/script/sea-worker-debug.js:295:5)'
    // Don't care about older browsers since web worker is an HTML5 feature
    var TRACE_RE = /.*?((?:http|https|file)(?::\/{2}[\w]+)(?:[\/|\.]?)(?:[^\s"]*)).*?/i
    // Try match `url` (Note: in IE there will be a tailing ')')
    var URL_RE = /(.*?):\d+:\d+\)?$/;
    // Find url of from stack trace.
    // Cannot simply read the first one because sometimes we will get:
    // Error
    //  at Error (native) <- Here's your problem
    //  at http://localhost:8000/_site/dist/sea.js:2:4334 <- What we want
    //  at http://localhost:8000/_site/dist/sea.js:2:8386
    //  at http://localhost:8000/_site/tests/specs/web-worker/worker.js:3:1
    while (stack.length > 0) {
      var top = stack.shift();
      m = TRACE_RE.exec(top);
      if (m != null) {
        break;
      }
    }
    var url;
    if (m != null) {
      // Remove line number and column number
      // No need to check, can't be wrong at this point
      var url = URL_RE.exec(m[1])[1];
    }
    // Set
    loaderPath = url
    // Set loaderDir
    loaderDir = dirname(url || cwd);
    // This happens with inline worker.
    // When entrance script's location.href is a blob url,
    // cwd will not be available.
    // Fall back to loaderDir.
    if (cwd === '') {
      cwd = loaderDir;
    }
  }else {
    var doc = document ;

    var scripts = doc.scripts ;

    //当前js文件加载后运行时的script脚本命令行
    var loaderScript = doc.getElementById("seajsnode") ||
        scripts[scripts.length - 1] ;

    function getScriptAbsoluteSrc(node) {
      return node.hasAttribute ? // non-IE6/7
          node.src :
        // see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
          node.getAttribute("src", 4)
    }

    loaderPath = getScriptAbsoluteSrc(loaderScript) ;

    // When `sea.js` is inline, set loaderDir to current working directory
    loaderDir = dirname(loaderPath || cwd)

    //console.log("loaderPath="+loaderPath+"    loaderDir="+loaderDir)
  }

  /**
   * util-request.js - The utilities for requesting script and style files
   * ref: tests/research/load-js-css/test.html
   */
  if (isWebWorker) {
    function requestFromWebWorker(url, callback, charset) {
      // Load with importScripts
      var error;
      try {
        importScripts(url);
      } catch (e) {
        error = e;
      }
      callback(error);
    }
    // For Developers
    seajs.request = requestFromWebWorker;
  }
  else {
    var doc = document ;

    var head = doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement ;

    var baseElement = head.getElementsByTagName("base")[0] ;

    var currentlyAddingScript ;

    function request(url, callback, charset) {

      var node = doc.createElement("script") ;

      if (charset) {
        var cs = isFunction(charset) ? charset(url) : charset ;
        if (cs) {
          node.charset = cs
        }
      }

      addOnload(node, callback, url) ;

      node.async = true ;
      node.src = url ;

      // For some cache cases in IE 6-8, the script executes IMMEDIATELY after
      // the end of the insert execution, so use `currentlyAddingScript` to
      // hold current node, for deriving url in `define` call
      currentlyAddingScript = node

      // ref: #185 & http://dev.jquery.com/ticket/2709
      baseElement ?
          head.insertBefore(node, baseElement) :
          head.appendChild(node) ;

      currentlyAddingScript = null
    }

    function addOnload(node, callback, url) {

      var supportOnload = "onload" in node ;

      if (supportOnload) {
        node.onload = onload ;
        node.onerror = function() {
          emit("error", { uri: url, node: node }) ;
          onload(true) ;
        }
      }else {
        node.onreadystatechange = function() {
          if (/loaded|complete/.test(node.readyState)) {
            onload() ;
          }
        }
      }

      function onload(error) {
        // Ensure only run once and handle memory leak in IE
        node.onload = node.onerror = node.onreadystatechange = null ;

        // Remove the script to reduce memory leak
        if (!data.debug) {
          head.removeChild(node) ;
        }

        // Dereference the node
        node = null ;

        callback(error) ;
      }
    }

    //For Developers,默认的request方法
    seajs.request = request
  }
  var interactiveScript ;

  function getCurrentScript() {
    if (currentlyAddingScript) {
      return currentlyAddingScript ;
    }

    /**
     * In non-IE browsers, the onload event is sufficient, it always fires
     immediately after the script is executed.
     * In IE, if the script is in the cache, it actually executes *during*
     the DOM insertion of the script tag, so you can keep track of which
     script is being requested in case define() is called during the DOM
     insertion.
     * In IE, if the script is not in the cache, when define() is called you
     can iterate through the script tags and the currently executing one will
     have a script.readyState == "interactive"
     **/
    if (interactiveScript && interactiveScript.readyState === "interactive") {
      return interactiveScript ;
    }
    var scripts = head.getElementsByTagName("script") ;

    for (var i = scripts.length - 1; i >= 0; i--) {
      var script = scripts[i] ;
      if (script.readyState === "interactive") {
        interactiveScript = script ;
        return interactiveScript ;
      }
    }

  }

  /**
   * 解析文件中的require依赖。
   * 主要解析方式为:
   * 1、提取第一个字符,如果是字母,则定位require开头的文本位置。
   * 2、解析出require内容,通过使用“'”或“"”
   * 通过正则解析require开头的字符,获取requrie内部的模块
   **/
  function parseDependencies(s) {
    if(s.indexOf('require') == -1) {
      return []
    }
    var index = 0, peek, length = s.length, isReg = 1, modName = 0, parentheseState = 0,
        parentheseStack = [], res = [] ;

    while(index < length) {
      readch() ;
      if(isBlank()) {

      }else if(isQuote()) {
        dealQuote()
        isReg = 1
      } else if(peek == '/') {
        readch()
        if(peek == '/') {
          index = s.indexOf('\n', index)
          if(index == -1) {
            index = s.length
          }
        }else if(peek == '*') {
          index = s.indexOf('*/', index)
          if(index == -1) {
            index = length
          }else {
            index += 2
          }
        }else if(isReg) {
          dealReg()
          isReg = 0
        }else {
          index--
          isReg = 1
        }
      }
      else if(isWord()) {
        dealWord()
      }
      else if(isNumber()) {
        dealNumber()
      }
      else if(peek == '(') {
        parentheseStack.push(parentheseState)
        isReg = 1
      }
      else if(peek == ')') {
        isReg = parentheseStack.pop()
      }
      else {
        isReg = peek != ']'
        modName = 0
      }
    }

    return res
    function readch() {
      peek = s.charAt(index++)
    }
    function isBlank() {
      return /\s/.test(peek)
    }
    function isQuote() {
      return peek == '"' || peek == "'"
    }
    //查找下一个引用字符的位置,并把当前被引用的内容加入到依赖列表中
    function dealQuote() {
      var start = index ;
      var c = peek ;
      //查找后续出现的引用字符
      var end = s.indexOf(c, start)
      if(end == -1) {
        index = length
      }else if(s.charAt(end - 1) != '\\') {
        index = end + 1
      }else {
        while(index < length) {
          readch()
          if(peek == '\\') {
            index++
          }
          else if(peek == c) {
            break
          }
        }
      }
        //判断是否已经解析了require开头的字符
      if(modName) {
        //单独存储依赖信息
        res.push(s.slice(start, index - 1))
        modName = 0
      }
    }
    function dealReg() {
      index--
      while(index < length) {
        readch()
        if(peek == '\\') {
          index++
        }
        else if(peek == '/') {
          break
        }
        else if(peek == '[') {
          while(index < length) {
            readch()
            if(peek == '\\') {
              index++
            }
            else if(peek == ']') {
              break
            }
          }
        }
      }
    }
    function isWord() {
      return /[a-z_$]/i.test(peek)
    }
    function dealWord() {
      var s2 = s.slice(index - 1)
      var r = /^[\w$]+/.exec(s2)[0];
      //判断是否为特殊的符号
      parentheseState = {
        'if': 1,
        'for': 1,
        'while': 1,
        'with': 1
      }[r]
      isReg = {
        'break': 1,
        'case': 1,
        'continue': 1,
        'debugger': 1,
        'delete': 1,
        'do': 1,
        'else': 1,
        'false': 1,
        'if': 1,
        'in': 1,
        'instanceof': 1,
        'return': 1,
        'typeof': 1,
        'void': 1
      }[r];
      //判断是否是require开头的字符。此处正则表达式用到了“反向向匹配”,其中“\1”指向前面“()”中的内容。
      modName = /^require\s*\(\s*(['"]).+?\1\s*\)/.test(s2) ;
      //如果符合以上格式,则只获取require('字符串
      if(modName) {
        r = /^require\s*\(\s*['"]/.exec(s2)[0]
        index += r.length - 2
      }else {
        index += /^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(s2)[0].length - 1
      }
    }
    function isNumber() {
      return /\d/.test(peek)
          || peek == '.' && /\d/.test(s.charAt(index))
    }
    function dealNumber() {
      var s2 = s.slice(index - 1)
      var r
      if(peek == '.') {
        r = /^\.\d+(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
      }
      else if(/^0x[\da-f]*/i.test(s2)) {
        r = /^0x[\da-f]*\s*/i.exec(s2)[0]
      }
      else {
        r = /^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(s2)[0]
      }
      index += r.length - 1
      isReg = 0
    }
  }
//========================module相关操作========================
//模块缓存,存储uri和module的映射
  var cachedMods = seajs.cache = {} ;

  var anonymousMeta ;

  //正在加载的模块
  var fetchingList = {} ;
  //已经获取的模块
  var fetchedList = {} ;
  //ajax请求完成之后module的回调函数。通过回调函数加载模块依赖的js
  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
  }

//seajs的基础模块类,seajs.use 或 require都是以该类为基础
  function Module(uri, deps) {
    this.uri = uri ;
    //存储依赖的名称
    this.dependencies = deps || [] ;

    //存储依赖名称和访问url的映射
    this.deps = {} ;

    //当前模块及依赖加载的加载的阶段
    this.status = 0 ;

    //当前模块的入口模块,例如使用seajs.use(id , factory),是一个顶层模块,其中id是entry,可以看做是一个入口
    //entry在按照依赖的层级,按照层次不断传播
    this._entry = [] ;
  }

  /**
   * 解析模块依赖,返回依赖模块的实际访问地址
   * @returns {Array}
   */
  Module.prototype.resolve = function() {
    var mod = this ;
    var ids = mod.dependencies ;
    var uris = [] ;

    //解析各个依赖的的url
    for (var i = 0, len = ids.length; i < len; i++) {
      uris[i] = Module.resolve(ids[i], mod.uri) ;
    }

    return uris ;
  }

  /**
   * 传递模块
   */
  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 (m.status < STATUS.LOADED && !entry.history.hasOwnProperty(m.uri)) {
          //记录已经传递依赖的entry
          entry.history[m.uri] = true ;
          count++ ;
          m._entry.push(entry) ;
          if(m.status === STATUS.LOADING) {
            m.pass() ;
          }
        }
      }

      //如果传递了entry到其依赖的模块中,就entry从当前模块中删掉。但是通过remain数量增加,来标识需要额外加载的模块
      if (count > 0) {
        //由于remain从1开始,包括当前模块。而开始的顶层依赖是没有module的及seajs.use
        entry.remain += count - 1 ;
        //将当前entry删掉,继续对下一个执行循环
        mod._entry.shift() ;
        i-- ;
      }

    }
  }

  /**
   * 装载模块的依赖
   */
  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 ;

    //解析依赖的各个插件的url
    var uris = mod.resolve() ;
    emit("load", uris) ;

    //在依赖中存储相关对象
    for (var i = 0, len = uris.length; i < len; i++) {
      //console.log("存储模块依赖子模块:"+mod.dependencies[i])
      mod.deps[mod.dependencies[i]] = Module.get(uris[i])
    }

    //每次状态完成在之后,都会执行load函数,在此处重新设置和检查依赖关系
    mod.pass() ;

    //如果当前模块的entry没有被传递,即没有依赖其它模块,则运行onload
    if (mod._entry.length) {
      mod.onload() ;
      return
    }

    //开始并行加载
    var requestCache = {} ;
    var m ;

    //开始加载依赖的模块
    for (i = 0; i < len; i++) {
      m = cachedMods[uris[i]] ;
      if (m.status < STATUS.FETCHING) {
        m.fetch(requestCache) ;
      }else if (m.status === STATUS.SAVED) {
        m.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] ;
      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) {
        //如果返回的是一个全局的object对象
      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

    /**
     * 传递到define的factory中作为参数
     * @param id
     * @returns {*|Array|{index: number, input: string}}
     */
    function require(id) {
      //模块已经在开始加载的时候解析完成了,此时可以直接获取相关的module
      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和seajs.use实现的功能是类似的
     * @param ids
     * @param callback
     * @returns {require}
     */
    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
  }

  /**
   * 远程加载文件
   * @param requestCache
   */
  Module.prototype.fetch = function(requestCache) {
    var mod = this ;
    var uri = mod.uri ;

    mod.status = STATUS.FETCHING ;

    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 ;
    }

    //如果当前获取uri中已包含指定的uri,则回调列表中加入当前module
    if (fetchingList.hasOwnProperty(requestUri)) {
      callbackList[requestUri].push(mod) ;
      return ;
    }

    fetchingList[requestUri] = true ;
    //将当前模块加入回调列表,当前模块加载完成之后,再去加载其依赖的其它模块
    callbackList[requestUri] = [mod] ;

    //触发request请求
    emit("request", emitData = {
      uri: uri,
      requestUri: requestUri,
      onRequest: onRequest,
      charset: isFunction(data.charset) ? data.charset(requestUri) || 'utf-8' : data.charset
    }) ;

    //标识是否已经由事件处理函数对请求做了处理
    if (!emitData.requested) {
      requestCache ?
          requestCache[emitData.requestUri] = sendRequest :
          sendRequest()
    }

    function sendRequest() {
      seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
    }

    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() ;
        }
      }
    }
  }

  /**
   * 解析模块的完整访问地址。id为依赖模块的id,refUri源模块的uri
   * @param id
   * @param refUri
   * @returns {*}
   */
  Module.resolve = function(id, refUri) {
    // Emit `resolve` event for plugins such as text plugin
    var emitData = { id: id, refUri: refUri } ;

    //console.log(id+"模块解析:"+refUri);

    //此处提现出了很好的扩展性,使用事件机制,优先调用注册的解析方法。如果解析失败,才掉用seajs本身的resolve方法
    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
    if (!isArray(deps) && isFunction(factory)) {
      deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString())
    }

    var meta = {
      id: id ,
      uri: Module.resolve(id) ,
      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
  Module.get = function(uri, deps) {
    return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
  }

// Use function is equal to load a anonymous module
  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
  seajs.use = function(ids, callback) {
    //debugger
    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
  }

  /**
   * config.js - The configuration for the loader
   */
// The root path to use for id2uri parsing
  data.base = loaderDir

// The loader directory
  data.dir = loaderDir

// The loader's full path
  data.loader = loaderPath

// The current working directory
  data.cwd = cwd

// The charset for requesting files
  data.charset = "utf-8"

// data.alias - An object containing shorthands of module id
// data.paths - An object containing path shorthands in module id
// data.vars - The {xxx} variables in module id
// data.map - An array containing rules to map module uri
// data.debug - Debug mode. The default value is false
  seajs.config = function(configData) {

    for (var key in configData) {
      var curr = configData[key] ;
      var prev = data[key] ;

      // Merge object config such as alias, vars
      if (prev && isObject(prev)) {
        for (var k in curr) {
          prev[k] = curr[k]
        }
      }else {
        // Concat array config such as map
        if (isArray(prev)) {
          //连接prev和curr数组
          curr = prev.concat(curr)
        }
        // Make sure that `data.base` is an absolute path
        else if (key === "base") {
          // Make sure end with "/"
          if (curr.slice(-1) !== "/") {
            curr += "/"
          }
          curr = addBase(curr)
        }

        // Set config
        data[key] = curr
      }
    }

    emit("config", configData)
    return seajs
  }

})(this);

 

seajs-wrap-debug.js,通过扩展request事件,对返回的js内容动态追加define(),避免在每个js中都要手写define函数。

/**
 * The Sea.js plugin for loading CommonJS file
 */
var global = window
var wrapExec = {}

seajs.on("resolve" , function(data) {
  var id = data.id
  if (!id) {
    return ""
  }

  // avoid seajs-css plugin conflict
  if (/\.css\.js$/.test(id)) {
    return;
  }
  
  var m = id.match(/[^?]+?(\.\w+)?(\?.*)?$/)
  // not parse those types
  var WhiteListReg = /\.(tpl|html|json|handlebars|css)/i

  if (m && (!WhiteListReg.test(m[1]) || !m[1])) {
    var uri = seajs.resolve(id, data.refUri)
    var query = m[2] || '';
    wrapExec[uri] = function(uri, content) {
      var wrappedContent;
//    var CMD_REG = /define\(.*function\s*\(\s*require\s*(.*)?\)\s*\{/;
      var CMD_REG = /define\(.*function\s*\(\s*(.*)?\)\s*\{/;
      //如果带有dowrap,则一定用define包装;否则根据是否带有define和nowrap判断是否封装
      if (uri.indexOf('dowrap')==-1&&(CMD_REG.test(content) ||
              query.indexOf('nowrap') > 0 || uri.indexOf('nowrap')>0)) {
        wrappedContent= content;
      } else {
        wrappedContent = 'define(function(require, exports, module) {\n' +
                        content + '\n})';
      }
      
      wrappedContent = wrappedContent + '//# sourceURL=' + uri;
      
      globalEval(wrappedContent, uri) ;
    }
    data.uri = uri
  }
})

seajs.on("request", function(data) {
  var exec = wrapExec[data.uri]

  if (exec) {

    xhr(data.requestUri, function(content) {
      exec(data.uri, content)
      data.onRequest()
    })

    data.requested = true
  }
})

// Helpers
function xhr(url, callback) {
  //modified , 解决ie10跨域加载js问题。首先使用XMLHttpRequest,ActiveXObject是ie7之前版本
  var r = global.XMLHttpRequest ? new global.XMLHttpRequest() :
      new global.ActiveXObject("Microsoft.XMLHTTP") ;

  try{
    r.open("GET", url, true) ;
  }catch (e){
    return failoverxhr(url, callback) ;
  }

  r.onreadystatechange = function() {

    if (r.readyState === 4) {
      // Support local file
      if (r.status > 399 && r.status < 600) {
        failoverxhr(url, callback) ;
      }else {
        callback(r.responseText)
      }
    }
  }

  //发送请求结果
  var result = null ;
  try{
    result = r.send(null) ;
  }catch (e){
    return failoverxhr(url, callback) ;
  }

  return result ;
}

/**
 * 静态资源请求失败的处理
 */
function failoverxhr(url, callback){

  var r = global.XMLHttpRequest ? new global.XMLHttpRequest() :
      new global.ActiveXObject("Microsoft.XMLHTTP") ;

  url = getLocalAppUrl(url) ;

  console.log("use backup "+url);

  r.open("GET", url, true) ;

  r.onreadystatechange = function() {
    if (r.readyState === 4) {
      // Support local file
      if (r.status > 399 && r.status < 600) {
        seajs.emit("failover error", {
          uri: url ,
          status: r.status
        }) ;
      }else {
        callback(r.responseText)
      }
    }
  }

  return r.send(null) ;
}

/**
 * 由本项目中获取请求的url
 */
function getLocalAppUrl(resourceURL) {
  //获取静态资源的uri
  var urlReg = /\/resources.*/ ;
  var uri = urlReg.exec(resourceURL) ;
  //如果配置了项目的url,则有url中请求静态资源
  if(uri && window.resourceConfig && window.resourceConfig.path){
    uri = window.resourceConfig.path + uri ;
  }
  return uri ;
}

function globalEval(content, uri) {
  if (content && /\S/.test(content)) {
    //global.execScript ||
    (function(content) {

      try {
        //var startDate = new Date();
        (global.eval || eval).call(global, content)
        //var endDate = new Date();
        //console.log(uri+" start-> "+(endDate.getTime() - startDate.getTime())+" ms") ;
      } catch(ex) {
        ex.fileName = uri ;
        console.error(ex) ;
      }
    })(content)
  }
}

seajs-css-debug.js : 支持seajs对css资源进行require,在git上提供了一个seajs-css插件,它是直接针对seajs源码进行的改动,有点不友好。以下代码是借助于seajs的事件机制实现css的加载。

 

(function(global){

    var doc = document ;
    var head = doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement ;
    var baseElement = head.getElementsByTagName("base")[0] ;

    //标识是否是css资源,"?:"取消“()”的捕获能力,只是用它来做分组
    var IS_CSS_RE = /\.css(?:\?|$)/i ;

    seajs.on("resolve" , function(data) {
        var id = data.id
        if (!id) {
            return ""
        }
        //只解析css结束的样式文件
        if (! IS_CSS_RE.test(id) ) {
            return ;
        }

        var uri = seajs.resolve(id, data.refUri) ;

        //如果解析后的uri是以".css.js"结尾,是因为seajs对于css加载本身不支持导致
        if( /\.css\.js$/.test(uri) ){
            uri = uri.substring(0 , uri.length - 3 ) ;
        }

        //console.log(id+" 解析地址: "+uri);

        data.uri = uri ;
    }) ;

    function cssOnload(node , data) {
        node.onload = node.onerror = node.onreadystatechange = null ;
        node = null ;

        //console.log("资源加载完成:"+data.requestUri) ;

        data.onRequest() ;
    }

    //只针对css进行处理
    seajs.on("request", function(data) {
        if(! IS_CSS_RE.test(data.requestUri) ){
            return ;
        }

        //console.log("css请求地址:"+data.requestUri) ;

        var node = document.createElement("link") ;

        if (data.charset) {
            node.charset = data.charset ;
        }

        //资源加载完成的处理函数
        var supportOnload = "onload" in node ;
        if (supportOnload) {
            node.onload = cssOnload.bind(global , node , data) ;
            node.onerror = function() {
                seajs.emit("error", { uri: url, node: node })
                cssOnload(node , data) ;
            }
        }else {
            node.onreadystatechange = function() {
                if (/loaded|complete/.test(node.readyState)) {
                    cssOnload(node , data) ;
                }
            }
        }

        //addOnload(node, callback, isCSS, url) ;

        node.rel = "stylesheet" ;
        node.href = data.requestUri ;

        //加入元素
        baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node) ;

        //标识资源已经在此进行了处理
        data.requested = true ;
    }) ;
})(window) ;

 

  seajs源码执行流程: 




  

  seajs源码中很好的设计,仅列出如下几点:

 

  • 事件机制。seajs通过事件机制实现扩展,例如插件:seajs-wrap 和 seajs-css 都是借助于seajs的事件机制进行的扩展。这块也可以在前端业务的框架中借鉴。
  • 依赖解析。在js加载完成之后,实际上执行的是define函数,在define中没有立刻执行factory函数,而是通过解析factory函数内容,获取其依赖模块并加载,待所有依赖的模块加载完成之后,再执行factory函数,即AMD推崇的依赖前置。此处也可以通过修改代码使用CMD的规则,即延迟加载依赖模块。
  • 模块加载。seajs通过模块加载解决了命名冲突(相同名称的变量或函数、多个不同名称或层级的命名空间管理)、模块化、版本化等问题。在js加载速度上seajs体现的并不明显,特别是现在浏览器都支持多个js文件同时加载的情况下。(seajs模块化根本上是将js的执行结果封装到了module.exports中,并通过参数的方式传递到factory中)
  • 正则表达式。在seajs源码中使用了很多正则对请求url、参数等进行判断和处理。通过seajs中正则,学习了正则表达式的贪婪匹配、非贪婪匹配、不捕捉模式、前瞻匹配、后瞻匹配、负前瞻匹配、负后瞻匹配、反向引用匹配等。

猜你喜欢

转载自lpyyn.iteye.com/blog/2380718