Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实例解析 SeaJS 内部执行过程 - 从 use 说起 #308

Closed
soda-x opened this issue Aug 1, 2012 · 24 comments
Closed

实例解析 SeaJS 内部执行过程 - 从 use 说起 #308

soda-x opened this issue Aug 1, 2012 · 24 comments

Comments

@soda-x
Copy link
Contributor

soda-x commented Aug 1, 2012

下面将通过一个例子来说明seajs内部是怎么运行的

代码中文注释已经在7月30日以issue的形式提交到seajs的github上

参考地址为:#305

假设1:

seajs为同步引入 并且 config的配置如下 ,并且在页面中head区写上<script src='./index.js'></script>

seajs.config({
    alias: {
        'jquery': 'jquery/1.7.2/jquery-debug.js'
    }
});

index.js :


seajs.use(['./a','jquery'],function(a,$){
    var num = a.a;
    $('#J_A').text(num);
})

a.js :


;define(function(require,exports,module){
    var b = require('./b');
    var a = function(){
        return 1 + parseInt(b.b());
    }
    exports.a = a;
})

b.js :


;define(function(require,exports,module){
    var c = require('./c');

    var b = function(){
        return 2 + parseInt(c.c());
    }
    exports.b = b;
})

c.js :


;define(function(require,exports,module){
    var c = function(){
        return 3;
    }
    exports.c = c;
})

由上述可知 a 依赖 b ,b依赖c

首先将会执行seajs本身,在这个过程中将会定义其中一些全局的方法,seajs多版本的容错等等 。

当程序进入到index.js


seajs.use(['./a','jquery'],function(a,$){
    var num = a.a;
    $('#J_A').text(num);
})

seajs将调用use方法 :


  // 从配置文件读取是否有需要提前加载的模块
  // 如果有预先加载模块,事先设置预加载模块为空,并加载预加载模块并执行回调,如果没有则顺序执行
  seajs.use = function(ids, callback) {
    var preloadMods = config.preload

    if (preloadMods.length) {
      // Loads preload modules before all other modules.
      globalModule._use(preloadMods, function() {
        config.preload = []
        globalModule._use(ids, callback)
      })
    }
    else {
      globalModule._use(ids, callback)
    }

    return seajs
  }

use方法将会从我们的config配置信息中查看 ,是否有预先需要被加载的模块,这边config中我们并没有相关设置,所以将直接 执行globalModule._use(ids, callback)

说明: globalModule 为seajs初始化时候Module的实例 var globalModule = new Module(util.pageUri, STATUS.COMPILED)

util.pageUri 为 页面地址



  function Module(uri, status) {

    this.uri = uri
    //如果status缺省,则赋值为0
    this.status = status || 0

    // this.id is set when saving
    // this.dependencies is set when saving
    // this.factory is set when saving
    // this.exports is set when compiling
    // this.parent is set when compiling
    // this.require is set when compiling
  }

此时 ids -> ['./a','jquery'] callback -> function(a,$){var num = a.a;$('#J_A').text(num);}

接下来将调用 globalModule._use(ids, callback)


  Module.prototype._use = function(ids, callback) {

    //查看传入的ids为字符串还是数组
    //如果传入的ids为字符串 例:ids =  './a' -> ids =['./a']
    //如果传入的ids为数组 例: ids = ['./a','./b'] -> ids = ['./a','./b'] (原样不变)
    util.isString(ids) && (ids = [ids])

    //得到uri 或者 uri数组

    var uris = resolve(ids, this.uri)


    this._load(uris, function() {
      //util.map : 让数据成员全部执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果
      var args = util.map(uris, function(uri) {

        return uri ? cachedModules[uri]._compile() : null

      })

      if (callback) {
        callback.apply(null, args)
      }
    })
  }

这里有需要解释 resolve_load 两个函数

此时 传递给 resolve的两个参数分别为 ids -> ['./a','jquery'] refUri -> 页面地址 例子中 地址为 http://localhost/test/SEAJS/test.html


  //resolve函数主要用于把ids -》 uris

  function resolve(ids, refUri) {
    if (util.isString(ids)) {
      // 是字符串的话执行Module._resolve -> util.id2Uri 
      // 在id2Uri这个函数中主要是把id转换为Uri(此uri被normalize过)
      return Module._resolve(ids, refUri)
    }
    // util.map 让数据成员全部执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果
    // 如果ids为数组 ,数组内成员执行resolve函数,并返回执行结果的新数组
    return util.map(ids, function(id) {
      return resolve(id, refUri)
    })
  }

此时由于 ids -> ['./a','jquery'] 为 数组 , 那么将通过map方法对数组每个成员都执行一次resolve方法,并返回新的数组。

最终执行 Module._resolve('./a', 'http://localhost/test/SEAJS/test.html') Module._resolve('jquery', 'http://localhost/test/SEAJS/test.html')

源码中 Module._resolve = util.id2Uri实际 Module._resolve 是 util.id2Uri的引用


  /**
   * Converts id to uri.
   * 把 id 转化为 uri
   */
  function id2Uri(id, refUri) {

    //id不存在则返回空
    if (!id) return ''

    //解析别名
    id = parseAlias(id)

    //如果refUri没有传入的时候 refUri采用pageUri
    refUri || (refUri = pageUri)

    var ret
    // 如果 传入的 id 本身就是一个绝对地址
    // absolute id
    if (isAbsolute(id)) {
      ret = id
    }
    // relative id
    else if (isRelative(id)) {
      // Converts './a' to 'a', to avoid unnecessary loop in realpath.
      if (id.indexOf('./') === 0) {
        id = id.substring(2)
      }
      ret = dirname(refUri) + id
    }
    // root id
    else if (isRoot(id)) {
      ret = refUri.match(ROOT_RE)[1] + id
    }
    // top-level id
    // 顶级标识 附加 base ,base为seajs路径
    else {
      ret = config.base + '/' + id
    }
    // 返回正常化的uri 
    return normalize(ret)
  }

代码运行到这边,首先,将去解析别名:id = parseAlias(id)

  /**
   * Parses alias in the module id. Only parse the first part.
   * 解析模块id中的别名。
   */
  function parseAlias(id) {
    // #xxx means xxx is already alias-parsed.
    // #xxx 意味着 xxx 别名已经解析完毕。

    //判断首个字符串字符是否为# 如果含#,则舍去# 并返回字符串
    if (id.charAt(0) === '#') {
      return id.substring(1)
    }

    // 获取用户配置的别名配置信息
    var alias = config.alias

    // Only top-level id needs to parse alias.
    // 顶级标识不以点(.)或斜线(/)或含有(://), 会相对模块系统的基础路径(即 SeaJS 的 base 路径)来解析
    if (alias && isTopLevel(id)) {

      //把id用(/)进行分割
      var parts = id.split('/')
      // 把首个数组的值做为关键字
      var first = parts[0]
      //如果alilas内含有first的属性
      if (alias.hasOwnProperty(first)) {
        //则把alias内first的属性值给parts[0],实则替换路径,替换成顶级标识
        parts[0] = alias[first]
        //重新拼接字符串
        id = parts.join('/')
      }
    }
    //返回 id
    return id
  }

id2Uri('./a', 'http://localhost/test/SEAJS/test.html') - > parseAlias('./a') return ./a -> http://localhost/test/SEAJS/a -> normalize(http://localhost/test/SEAJS/a ) -> http://localhost/test/SEAJS/a .js

id2Uri('jquery', 'http://localhost/test/SEAJS/test.html') - > parseAlias('jquery') return jquery/1.7.2/jquery-debug.js -> http://localhost/test/SEAJS/lib//jquery/1.7.2/jquery-debug.js ->normalize(http://localhost/test/SEAJS/lib//jquery/1.7.2/jquery-debug.js) -> http://localhost/test/SEAJS/lib/jquery/1.7.2/jquery-debug.js

最终

var uris = resolve(ids, this.uri)
uris['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js']

模块路径解析已经完毕,而接下来让我们再次返回Module_use 方法

此时 uris ->['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'] callback -> function(a,$){var num = a.a;$('#J_A').text(num);}

接下来将执行Module._load()方法。


  // _load()方法主要会先判断那些资源文件还没有ready,如果全部资源文件都处于ready状态就执行callback()
  // 在这其中还会做循环依赖的判断,以及对没有加载的js执行加载

  Module.prototype._load = function(uris, callback) {

    //util.filter : 让数据成员全部执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后返回为true的成员
    //unLoadedUris是那些没有被编译的模块uri数组
    var unLoadedUris = util.filter(uris, function(uri) {

      //返回执行函数后布尔值为true的成员,在uri存在并且在内部变量cacheModules中不存在或者或者它在存储信息中status的值小于STATUS.READY时返回true
      // STATUS.READY值为4,小于四则可能的情况是获取中,下载中或者还有依赖模块未模块信息saved
      return uri && (!cachedModules[uri] ||
          cachedModules[uri].status < STATUS.READY)

    })

    //如果模块所依赖的模块全部被加载执行了,执行回调并退出函数体
    var length = unLoadedUris.length
    if (length === 0) {
      callback()
      return
    }
    //还未加载的模块个数
    var remain = length
    //创建闭包,尝试去加载那些没有加载的模块
    for (var i = 0; i < length; i++) {
      (function(uri) {
        //判断如果在内部变量cachedModules里面并不存在该uri的存储信息则实例化一个Module对象
        var module = cachedModules[uri] ||
            (cachedModules[uri] = new Module(uri, STATUS.FETCHING))

        //如果模块的状态值大于等于2,也就意味着模块已经被下载好并已经存在于本地了
        //这个时候执行onFetched()
        //否则则调用fetch(uri, onFetched) ,尝试下载资源文件,onload后执行回调onFetched方法

        module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)

        function onFetched() {
          // cachedModules[uri] is changed in un-correspondence case

          module = cachedModules[uri]

          //但模块的状态值为大于等于STATUS.SAVED的时候,也就意味着该模块所有的依赖信息已经被拿到
          if (module.status >= STATUS.SAVED) {

            //getPureDependencies:得到不存在循环依赖的依赖数组
            var deps = getPureDependencies(module)

            //如果依赖数组不为空
            if (deps.length) {
              //再次执行_load()方法,直到全部依赖加载完成后执行回调
              Module.prototype._load(deps, function() {
                cb(module)
              })
            }
            //如果依赖数组为空的情况下,直接执行cb(module)
            else {
              cb(module)             
            }
          }
          // Maybe failed to fetch successfully, such as 404 or non-module.
          // In these cases, module.status stay at FETCHING or FETCHED.
          // 如果获取失败后,比如404或者不符合模块化规范
          //在这种情形下,module.status会维持在 FETCHING 或者 FETCHED
          else {
            cb()
          }
        }

      })(unLoadedUris[i])
    }
    // cb 方法 - 加载完所有模块执行回调
    function cb(module) {

      // 如果module的存储信息存在,那么修改它的module存储信息中的status的值,修改为 STATUS.READY
      module && (module.status = STATUS.READY)
      // 只有当所有模块加载完毕后执行回调。
      --remain === 0 && callback()
    }
  }

进入到_load(uris, callback)方法

uris ->

['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js']

callback - >


function() {
      //util.map : 让数据成员全部执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果
      var args = util.map(uris, function(uri) {

        return uri ? cachedModules[uri]._compile() : null

      })

      if (callback) {
        callback.apply(null, args)
      }
})

其首先第一步做的事情就是 在 cachedModules[uri]中查找 是否存在 该uri 的 存储信息,如果存在存储信息,则表明该模块已经被下载好了,但是有可能它所依赖的模块还没有下载好 ,如果没有信息则说明该模块需要被下载。

关于模块状态:推荐直接阅读中文版注释代码#305 或者 玉伯的文章#303

经过第一步 我们将会得到unLoadedUris -> ['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js']

如果unLoadedUris 为空 那么直接执行_load的回调callback

这里unLoadedUris的数组长度为2 所以 接下来 会产生两个 以 js路径为名称的闭包

http://localhost/test/SEAJS/a.js为例
接下来 : 首先会创建一个Module

cachedModules('http://localhost/test/SEAJS/a.js') = new Module('http://localhost/test/SEAJS/a.js',1)

初始化 a模块存储信息

module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)

因为此时模块并没有加载 所以 接下来将会执行 fetch(uri, onFetched)fetch('http://localhost/test/SEAJS/a.js',onFetched)



  function fetch(uri, callback) {
    // 根据map中的规则替换uri为新的请求地址
    var requestUri = util.parseMap(uri)

    // 首先在已获取列表中查找是否含有requestUri记录
    if (fetchedList[requestUri]) {
      // See test/issues/debug-using-map
      // 这个时候将原始uri的module存储信息 刷新到 通过map重定义的requestUri上
      cachedModules[uri] = cachedModules[requestUri]
      // 执行callback 并返回
      callback()
      return
    }

    //在获取列表中查询 requestUri 的存储信息
    if (fetchingList[requestUri]) {
      //在callbacklist中加入该uri对应下的callback 并返回
      callbackList[requestUri].push(callback)
      return
    }
    // 如果尝试获取的模块都未出现在fetchedList和fetchingList中,则分别在请求列表和回调列表中添加其信息
    fetchingList[requestUri] = true
    callbackList[requestUri] = [callback]

    // Fetches it
    Module._fetch(
        requestUri,

        function() {
          fetchedList[requestUri] = true

          // Updates module status
          // 如果 module.status 等于 STATUS.FECTCHING ,则修改module状态为FETCHED
          var module = cachedModules[uri]
          if (module.status === STATUS.FETCHING) {
            module.status = STATUS.FETCHED
          }

          // Saves anonymous module meta data
          // 如果存在匿名模块元信息
          if (anonymousModuleMeta) {

            save(uri, anonymousModuleMeta)
            anonymousModuleMeta = null
          }

          // Assigns the first module in package to cachedModules[uri]
          // See: test/issues/un-correspondence
          if (firstModuleInPackage && module.status === STATUS.FETCHED) {
            cachedModules[uri] = firstModuleInPackage
            firstModuleInPackage.packageUri = uri
          }
          firstModuleInPackage = null

          // Clears 清除获取信息
          if (fetchingList[requestUri]) {
            delete fetchingList[requestUri]
          }

          // Calls callbackList 统一执行回调
          if (callbackList[requestUri]) {
            util.forEach(callbackList[requestUri], function(fn) {
              fn()
            })
            delete callbackList[requestUri]
          }

        },

        config.charset
    )
  }

进入 fetch 函数 , 首先会根据请求的urlmap 去做一次匹配,如果map中已经存在相关规则,那么替换uri为匹配规则后的uri 因为这里map并没有做任何设置,所以不会对uri做任何处理
紧接着 http://localhost/test/SEAJS/a.js 会在 已请求 和 正在请求 的队列中查找 是否存在

如果不存在 那么 a.js 将被 添加到 请求队列中 其回调也将被添加到回调列表中

接下来 将会执行 Module._fetch()


  // Module._fetch  资源文件加载
  Module._fetch = util.fetch

实质 将会执行 util.fetch   

  //加载资源文件文件

  util.fetch = function(url, callback, charset) {

    //获取的文件是不是css
    var isCSS = IS_CSS_RE.test(url)

    //如果是css创建节点 link  否则 则创建script节点
    var node = document.createElement(isCSS ? 'link' : 'script')

    //如果存在charset 如果charset不是function类型,那就直接对节点设置charset ,如果是function如下例:
    /*
    seajs.config({
      charset: function(url) {
      // xxx 目录下的文件用 gbk 编码加载
        if (url.indexOf('http://example.com/js/xxx') === 0) {
          return 'gbk';
        }
      // 其他文件用 utf-8 编码
        return 'utf-8';
      }

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

    //assets执行完毕后执行callback ,如果自定义callback为空,则赋予noop 为空函数
    assetOnload(node, callback || noop)

    //如果是样式 ……  如果是 脚本 …… async 详见:https://github.com/seajs/seajs/issues/287
    if (isCSS) {
      node.rel = 'stylesheet'
      node.href = url
    }
    else {
      node.async = 'async'
      node.src = url
    }

   // For some cache cases in IE 6-9, the script executes IMMEDIATELY after
    // the end of the insertBefore execution, so use `currentlyAddingScript`
    // to hold current node, for deriving url in `define`.
    // 之下这些代码都是为了兼容ie 
    // 假如A页面在含有base标签,此时A页面有个按钮具有请求B页面的功能,并且请求过来的内容将插入到A页面的某个div中
    // B页面有一些div,并且包含一个可执行的script
    // 其他浏览器都会在异步请求完毕插入页面后执行该script 但是 ie 不行,必须要插入到base标签前。
    currentlyAddingScript = node

    // ref: #185 & http://dev.jquery.com/ticket/2709 
    // 关于base 标签 http://www.w3schools.com/tags/tag_base.asp

    baseElement ?
        head.insertBefore(node, baseElement) :
        head.appendChild(node)

    currentlyAddingScript = null
  }

  //资源文件加载完毕后执行回调callback
  function assetOnload(node, callback) {
    if (node.nodeName === 'SCRIPT') {
      scriptOnload(node, callback)
    } else {
      styleOnload(node, callback)
    }
  }

  //资源文件加载完执行回调不是所有浏览器都支持一种形式,存在兼容性问题
  //http://www.fantxi.com/blog/archives/load-css-js-callback/ 这篇文章非常不错

  //加载脚本完毕后执行回调
  function scriptOnload(node, callback) {

    // onload为IE6-9/OP下创建CSS的时候,或IE9/OP/FF/Webkit下创建JS的时候  
    // onreadystatechange为IE6-9/OP下创建CSS或JS的时候

    node.onload = node.onerror = node.onreadystatechange = function() {

      //正则匹配node的状态
      //readyState == "loaded" 为IE/OP下创建JS的时候
      //readyState == "complete" 为IE下创建CSS的时候 -》在js中做这个正则判断略显多余
      //readyState == "undefined" 为除此之外浏览器
      if (READY_STATE_RE.test(node.readyState)) {

        // Ensure only run once and handle memory leak in IE
        // 配合 node = undefined 使用 主要用来确保其只被执行一次 并 处理了IE 可能会导致的内存泄露
        node.onload = node.onerror = node.onreadystatechange = null

        // Remove the script to reduce memory leak
        // 在存在父节点并出于非debug模式下移除node节点
        if (node.parentNode && !config.debug) {
         head.removeChild(node)
        }

        // Dereference the node
        // 废弃节点,这个做法其实有点巧妙,对于某些浏览器可能同时支持onload或者onreadystatechange的情况,只要支持其中一种并执行完一次之后,把node释放,巧妙实现了可能会触发多次回调的情况
        node = undefined

        //执行回调
        callback()
      }
    }

  }

  //加载样式完毕后执行回调
  function styleOnload(node, callback) {

    // for Old WebKit and Old Firefox
    // iOS 5.1.1 还属于old --! 但是 iOS6中 536.13
    // 这里用户采用了代理可能会造成一点的勿扰,可能代理中他是一个oldwebkit浏览器 但是实质却不是
    if (isOldWebKit || isOldFirefox) {
      util.log('Start poll to fetch css')

      setTimeout(function() {
        poll(node, callback)
      }, 1) // Begin after node insertion 
      // 延迟执行 poll 方法,确保node节点已被插入
    }
    else {
      node.onload = node.onerror = function() {
        node.onload = node.onerror = null
        node = undefined
        callback()
      }
    }

  }

  function poll(node, callback) {
    var isLoaded

    // for WebKit < 536
    // 如果webkit内核版本低于536则通过判断node节点时候含属性sheet
    if (isOldWebKit) {
      if (node['sheet']) {
        isLoaded = true
      }
    }
    // for Firefox < 9.0
    else if (node['sheet']) {
      try {
        //如果存在cssRules属性
        if (node['sheet'].cssRules) {
          isLoaded = true
        }
      } catch (ex) {
        // The value of `ex.name` is changed from
        // 'NS_ERROR_DOM_SECURITY_ERR' to 'SecurityError' since Firefox 13.0
        // But Firefox is less than 9.0 in here, So it is ok to just rely on
        // 'NS_ERROR_DOM_SECURITY_ERR'

        // 在Firefox13.0开始把'NS_ERROR_DOM_SECURITY_ERR'改成了'SecurityError'
        // 但是这边处理是小于等于firefox9.0的所以在异常处理上还是依赖与'NS_ERROR_DOM_SECURITY_ERR'
        if (ex.name === 'NS_ERROR_DOM_SECURITY_ERR') {
          isLoaded = true
        }
      }
    }

    setTimeout(function() {
      if (isLoaded) {
        // Place callback in here due to giving time for style rendering.
        callback()
      } else {
        poll(node, callback)
      }
    }, 1)
  }

  function noop() {
  }

seajsfetch 其实算是非常非常重要的一个函数, 他把控了资源的加载,其中内部存在了大量兼容性代码,主要有针对ie6 baseElement的问题 , oldWebkit的问题 , 以及 css 加载监听的兼容,如何兼容暂不谈,因为在以上代码中我也非常详细阐述了。

首先fetch函数会查看 需要加载的资源是 css 文件还是 js 文件

这里 http://localhost/test/SEAJS/a.js 是一个脚本文件 所以这个时候会动态创建一个空的script对象 设置 charset 值

并最终执行 assetOnload(node, callback || noop)


  function assetOnload(node, callback) {
    if (node.nodeName === 'SCRIPT') {
      scriptOnload(node, callback)
    } else {
      styleOnload(node, callback)
    }
  }

所以接下来会执行 scriptOnload函数 最终 head.appendChild(node)该节点将被插入到页面的head中
<script charset="utf-8" async="" src="http://localhost/test/SEAJS/a.js"></script>
这个时候 js 将被正式下载 但运行将被阻塞 。

同样道理

<script charset="utf-8" async="" src="http://localhost/test/SEAJS/libs/jquery/1.7.2/jquery.js"></script>

到此 seajs.use 到此完毕 。 接下来 资源加载完毕之后将运行a.js 随后将触发onload 事件

a.js运行之时 会 触发define 的执行


  //define 定义 ,id : 模块id , deps : 模块依赖 , factory

  Module._define = function(id, deps, factory) {
    var argsLength = arguments.length

    // define(factory)
    // 如果参数只有一位,那默认为factory,其余配对信息将由打包工具完成,需要走注意的一点是factory不可缺省!
    if (argsLength === 1) {
      factory = id
      id = undefined
    }
    // define(id || deps, factory)
    // 如果参数有两位
    else if (argsLength === 2) {
      //赋值factory为deps -》 因为参数缺省导致这么书写
      factory = deps
      // deps 为 undefined
      deps = undefined

      //判断id时候为一个数组,
      // define(deps, factory)
      if (util.isArray(id)) {
        deps = id
        id = undefined
      }
    }

    // Parses dependencies.
    //解析依赖关系
    // 如果deps不是数组类型,同时factory是函数
    if (!util.isArray(deps) && util.isFunction(factory)) {
      // 函数体内正则匹配require字符串,并形成数组返回赋值给deps
      deps = util.parseDependencies(factory.toString())
    }

    //设置元信息
    var meta = { id: id, dependencies: deps, factory: factory }
    var derivedUri

    // Try to derive uri in IE6-9 for anonymous modules.
    if (document.attachEvent) {
      // Try to get the current script.
      // 得到当前script的节点
      var script = util.getCurrentScript()
      // 如果script节点存在 
      if (script) {
        // 得到原始uri地址
        derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script))
      }
      if (!derivedUri) {
        util.log('Failed to derive URI from interactive script for:',
            factory.toString(), 'warn')

        // NOTE: If the id-deriving methods above is failed, then falls back
        // to use onload event to get the uri.
      }
    }

    // Gets uri directly for specific module.
    var resolvedUri = id ? resolve(id) : derivedUri

    if (resolvedUri) {
      // If the first module in a package is not the cachedModules[derivedUri]
      // self, it should assign it to the correct module when found.
      if (resolvedUri === derivedUri) {
        var refModule = cachedModules[derivedUri]
        if (refModule && refModule.packageUri &&
            refModule.status === STATUS.SAVED) {
          cachedModules[derivedUri] = null
        }
      }

      var module = save(resolvedUri, meta)

      // Handles un-correspondence case:
      if (derivedUri) {
        // cachedModules[derivedUri] may be undefined in combo case.
        if ((cachedModules[derivedUri] || {}).status === STATUS.FETCHING) {
          cachedModules[derivedUri] = module
          module.packageUri = derivedUri
        }
      }
      else {
        firstModuleInPackage || (firstModuleInPackage = module)
      }
    }
    else {
      // Saves information for "memoizing" work in the onload event.
      anonymousModuleMeta = meta
    }

  }

接着分析 , 那么 接下来将会执行 definedefine首先会对factory执行一个判断 ,判断它是否为一个函数(原因是因为define内也可以包括文件,对象)

如果是函数 , 那么 就会 通过factory.toString(),得到函数 并通过 正则匹配 得到 a.js的依赖 并 把依赖保存在 deps

对于 a.js 而言 它的依赖 是 b.js 所以 deps['./b']

并对 a.js 的信息进行保存 var meta = { id: id, dependencies: deps, factory: factory }

针对a.js meta = { id : undefined , dependencies : ['./b'] , factory : function(xxx){xxx}}

在 ie 6-9 浏览器中可以拿到当前运行js的路径 但是在标准浏览器中 这不可行 所以暂时先把 元信息赋值给anonymousModuleMeta = meta

define执行完毕后 onload事件 将被 触发

这个时候将直接执行Module._fetch(requestUri,callback,config.charset)中的callback


          fetchedList[requestUri] = true

          // Updates module status
          // 如果 module.status 等于 STATUS.FECTCHING ,则修改module状态为FETCHED
          var module = cachedModules[uri]
          if (module.status === STATUS.FETCHING) {
            module.status = STATUS.FETCHED
          }

          // Saves anonymous module meta data
          // 如果存在匿名模块元信息
          if (anonymousModuleMeta) {

            save(uri, anonymousModuleMeta)
            anonymousModuleMeta = null
          }

          // Assigns the first module in package to cachedModules[uri]
          // See: test/issues/un-correspondence
          if (firstModuleInPackage && module.status === STATUS.FETCHED) {
            cachedModules[uri] = firstModuleInPackage
            firstModuleInPackage.packageUri = uri
          }
          firstModuleInPackage = null

          // Clears 清除获取信息
          if (fetchingList[requestUri]) {
            delete fetchingList[requestUri]
          }

          // Calls callbackList 统一执行回调
          if (callbackList[requestUri]) {
            util.forEach(callbackList[requestUri], function(fn) {
              fn()
            })
            delete callbackList[requestUri]
          }

        }

callback执行过程中 , 修改当前回调 模块的 状态值 , 将其设置为 module.status = STATUS.FETCHED

因为在 ie 6-9 浏览器中可以拿到当前运行js的路径 但是在标准浏览器中 这不可行 所以暂时先把 元信息赋值给 anonymousModuleMeta = meta

所以接下来 判断 是否存在anonymousModuleMeta

判断存在 其值为 :{ id : undefined , dependencies : ['./b'] , factory : function(xxx){xxx}}

接下来将触发save(uri,meta)


  function save(uri, meta) {
    //尝试获取uri的存储信息,如果存在则直接从内部变量cachedModules中获取,否则新建一个Module对象
    var module = cachedModules[uri] || (cachedModules[uri] = new Module(uri))

    // Don't override already saved module
    // 
    if (module.status < STATUS.SAVED) {
      // Lets anonymous module id equal to its uri
      // 让那些匿名模块的id 等于 meta.id 如果meta.id不存在 则为 uri
      module.id = meta.id || uri

      module.dependencies = resolve(
          // 返回那些存在依赖关系的module的dependencies的绝对地址
          util.filter(meta.dependencies || [], function(dep) {
            return !!dep
          }), uri)


      module.factory = meta.factory

      // Updates module status 
      // 更新模块状态 为saved
      // 假设存在模块a ,在 onload 事件触发前,a.js 中的 define 代码已执行。
      // 但由于是匿名模块,该模块的 uri 信息,需要等到 onload 触发后才能获取到。
      // 获取到后,会将模块 a 的信息存储到内部变量 cachedModules 里,此时模块状态变成 STATUS.SAVED
      module.status = STATUS.SAVED
    }
    // 返回module
    return module
  }

该函数首先会判断其是否存在cachedModules中 ,如果不存在将新建一个Module实例

这边模块 a.js 已经存在在cachedModules 具体信息为:


module: Module
status: 2
uri: "http://localhost/test/SEAJS/a.js"

接下来 将 设置module更多的信息 , 这些信息来源于 a.js define执行过程中所产生的一些信息 。

设置a模块的id , 依赖关系 (依赖关系将通过resolve,统一转换成绝对路径),factory , 模块状态为module.status = STATUS.SAVED

经过一系列转换后 a 的模块信息为 :


module: Module
dependencies: Array[1]
0: "http://localhost/test/SEAJS/b.js"
length: 1
factory: function (require,exports,module){
id: "http://localhost/test/SEAJS/a.js"
status: 3
uri: "http://localhost/test/SEAJS/a.js"

然后清空 anonymousModuleMeta 信息 , 避免之后加载信息出乱 并清空 firstModuleInPackage = null

firstModuleInPackage 意在 #137

Handle un-correspondence case

信息不配对的情况

再接下来 将统一 执行 回调队列 callbackList 中的 a.js 所 对应的回调。

callbackList为:


callbackList
Object
http://localhost/test/SEAJS/a.js: Array[1]
0: function onFetched() {
length: 1
__proto__: Array[0]
http://localhost/test/SEAJS/libs/jquery/1.7.2/jquery-debug.js: Array[1]
0: function onFetched() {
length: 1
__proto__: Array[0]
__proto__: Object

所以 a.js 的callbackonFetched , 因为onFteched存在与闭包之中,而闭包保存了之前的状态,所以函数将在之前状态的情况下 继续执行


 (function(uri) {
        //判断如果在内部变量cachedModules里面并不存在该uri的存储信息则实例化一个Module对象
        var module = cachedModules[uri] ||
            (cachedModules[uri] = new Module(uri, STATUS.FETCHING))

        //如果模块的状态值大于等于2,也就意味着模块已经被下载好并已经存在于本地了
        //这个时候执行onFetched()
        //否则则调用fetch(uri, onFetched) ,尝试下载资源文件,onload后执行回调onFetched方法

        module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)

        function onFetched() {
          // cachedModules[uri] is changed in un-correspondence case

          module = cachedModules[uri]

          //但模块的状态值为大于等于STATUS.SAVED的时候,也就意味着该模块所有的依赖信息已经被拿到
          if (module.status >= STATUS.SAVED) {

            //getPureDependencies:得到不存在循环依赖的依赖数组
            var deps = getPureDependencies(module)

            //如果依赖数组不为空
            if (deps.length) {
              //再次执行_load()方法,直到全部依赖加载完成后执行回调
              Module.prototype._load(deps, function() {
                cb(module)
              })
            }
            //如果依赖数组为空的情况下,直接执行cb(module)
            else {
              cb(module)             
            }
          }
          // Maybe failed to fetch successfully, such as 404 or non-module.
          // In these cases, module.status stay at FETCHING or FETCHED.
          // 如果获取失败后,比如404或者不符合模块化规范
          //在这种情形下,module.status会维持在 FETCHING 或者 FETCHED
          else {
            cb()
          }
        }

      })(unLoadedUris[i])

接下来 把 a.js 的存储信息直接从cachedModules['http://localhost/test/SEAJS/a.js']获得 ,内容如下:


module: Module
dependencies: Array[1]
0: "http://localhost/test/SEAJS/b.js"
factory: function (require,exports,module){
id: "http://localhost/test/SEAJS/a.js"
status: 3
uri: "http://localhost/test/SEAJS/a.js"

接下来 getPureDependencies(module)方法 将执行 //获取纯粹的依赖关系 , 得到不存在循环依赖关系的依赖数组


  function getPureDependencies(module) {

    var uri = module.uri

    return util.filter(module.dependencies, function(dep) {

      //是先推入被检查模块的uri到循环依赖检查栈中

      circularCheckStack = [uri]

      //接下来检查模块uri是否和其依赖的模块存在循环依赖
      //传入的两个值分别为cachedModules[dep]:依赖模块的存储信息 , 当前被检查的模块的uri
      //ps:cachedModules[dep] 这个内部变量的存在时,代表其模块状态已经为saved

      var isCircular = isCircularWaiting(cachedModules[dep], uri)

      //如果存在循环依赖则在检查栈中推入uri , 并打印循环依赖信息
      if (isCircular) {
        circularCheckStack.push(uri)
        printCircularLog(circularCheckStack)
      }
      // 如果存在循环依赖返回false,如果不存在则返回true
      return !isCircular
    })
  }

  //检查模块间是否存在循环等待,返回布尔值

  function isCircularWaiting(module, uri) {
    // 如果主模块所依赖模块存储信息不存在 或者 模块的状态值等于saved ,那么他就不属于循环等待的情况
    // 因为模块状态为saved的时候代表该模块的信息已经被存储到了内部变量cacheModules内
    if (!module || module.status !== STATUS.SAVED) {
      return false
    }

    //反之,将该依赖模块的uri信息将被推入到循环检查栈

    circularCheckStack.push(module.uri)

    //获取依赖模块的依赖
    var deps = module.dependencies

    //如果依赖模块没有对别的模块产生依赖,则直接返回 false
    if (deps.length) {
      // 如果依赖模块存在对别的模块有依赖 
      // 那么接下去将会在依赖关系中查找是否存在依赖uri的情形,存在返回true
      //util.indexOf:返回指定字符串在某个数组成员匹配中首次全文匹配的索引,如果没有匹配则返回 -1
      if (util.indexOf(deps, uri) > -1) {
        return true
      }

      //如果不存在上述情形,那么进一步查看,依赖模块的依赖模块,查看他们是否存在对当前模块存在依赖,如果存在返回true
      //逐层检查
      for (var i = 0; i < deps.length; i++) {
        if (isCircularWaiting(cachedModules[deps[i]], uri)) {
          return true
        }
      }

      //如果经过这两步的判断还是没有发现依赖,那就只能返回false了,认为没有循环依赖

      return false
    }

    return false
  }

  //打印存在循环依赖
  function printCircularLog(stack, type) {
    util.log('Found circular dependencies:', stack.join(' --> '), type)
  }

这些函数的意义就在与 剔除 a.js 中 是否 存在 对于自身的 依赖

经过这一步 var deps = getPureDependencies(module)

依赖关系为


deps: Array[1]
0: "http://localhost/test/SEAJS/b.js"

紧接着 这个时候会


Module.prototype._load(deps, function() {
    cb(module)
})


    function cb(module) {

      // 如果module的存储信息存在,那么修改它的module存储信息中的status的值,修改为 STATUS.READY
      module && (module.status = STATUS.READY)
      // 只有当所有模块加载完毕后执行回调。
      --remain === 0 && callback()
    }

对模块a所依赖的b.js 执行_load()

这个时候就b.js 会再次重复 a.js 所执行的一切 。

此时会先执行 jq define 因为jq对其他无依赖所以 正式返回module并设置了其状态为ready


Module
dependencies: Array[0]
factory: function (require) {
id: "#jquery/1.7.2/jquery-debug"
status: 4
uri: "http://localhost/SEAJS/libs/jquery/1.7.2/jquery-debug.js"

再接下来将执行 b 的define , define 首先会对factory执行一个判断 ,判断它是否为一个函数(原因是因为define内也可以包括文件,对象)

如果是函数 , 那么 就会 通过 factory.toString(),得到函数 并通过 正则匹配 得到 b.js的依赖 并 把依赖保存在 deps 中

对于 b.js 而言 它的依赖 是 c.js 所以 deps['./c']

并对 b.js 的信息进行保存 var meta = { id: id, dependencies: deps, factory: factory }

针对b.js meta = { id : undefined , dependencies : ['./c'] , factory : function(xxx){xxx}}

在 ie 6-9 浏览器中可以拿到当前运行js的路径 但是在标准浏览器中 这不可行 所以暂时先把 元信息赋值给 anonymousModuleMeta = meta

执行b的define 执行完毕后 onload事件 将被 触发

之后再会加载c.js , 执行和 b.js加载一致 , 这个时候加载完c.js

执行c的define 继而onload事件被触发 由于c无依赖 c 状态 被 修改为 ready

进而 b 修改为ready

a 修改为ready

当所有模块都为ready之后 开始 执行回调


 this._load(uris, function() {
      //util.map : 让数据成员全部执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果
      var args = util.map(uris, function(uri) {

        return uri ? cachedModules[uri]._compile() : null

      })

      if (callback) {
        callback.apply(null, args)
      }
    })

回调过程中开始编译各个模块

首先将编译 a.js


  function runInModuleContext(fn, module) {
    var ret = fn(module.require, module.exports, module)

    // 在ret不为undefined情况下 ,赋值该module的exports值为fn的执行结果
    if (ret !== undefined) {
      module.exports = ret
    }
  }

编译代码和过程省略,因为结合源代码 这一块相对没有那么绕 所以不做特别多的说明。

假设模块 a 的 factory 执行时,假设a内部含有b的依赖,因此也会触发模块 b 的执行,模块 b 有可能还有依赖模块,比如 c,这时会继续触发模块 c 的执行,这就形成一个 stack:
// 这个信息,就存储在内部变量 compileStack 里。
/*
模块 a 开始执行
模块 b 开始执行
模块 c 开始执行
模块 c 执行完毕
模块 b 执行完毕
模块 a 执行完毕
*/

等编译都完成


      if (callback) {
        callback.apply(null, args)
      }

这里的args 为:


args: Array[2]
0: Object
1: function ( selector, context ) {

也就是说这里的


function(a,$){
    var num = a.a;
    $('#J_A').text(num);
}

把a的module.exports 和 jquery 的 module.exports 传递 给 a , $

@lifesinger
Copy link
Member

感谢 @pigcan ,太给力了!

@honingwon
Copy link

建议除SeaJS之外的文件都异步加载。鸡蛋里挑骨头 >.<

@ziluo
Copy link

ziluo commented Aug 2, 2012

mark~~

@SMbey0nd
Copy link

SMbey0nd commented Aug 2, 2012

牛B,mark先

@nuintun
Copy link

nuintun commented Aug 8, 2012

标记下,回来认真看~~~

@jutleo
Copy link

jutleo commented Sep 3, 2012

mark,i will go back

@island205
Copy link

看了这里发现:原来preload并没有预加载啊,更像基础依赖必须加载。如果a->b->c->jQuery的话,配置了preload:jQuery,jQuery确实在加载树中提前了,但是与通常的预加载不太一样。

@gaoli
Copy link

gaoli commented Nov 8, 2012

花了一天时间,完整看了一遍,优雅的代码令人印象深刻~

各种回调脑子不够用,只能画着流程图方便记忆。

不晓得 @pigcan 写这个花了多长时间~ 佩服!

@chan056
Copy link

chan056 commented Nov 17, 2012

看晕了,什么时候才能写出这样的代码

@Fenixgo
Copy link

Fenixgo commented Jun 25, 2013

好长,得仔细研读

@fe-ninja
Copy link

不错。。需要反复看

@lichunqiang
Copy link

mark

@chaojidan7252
Copy link

先下载a.js,后下载jquery.js,那哪个先下载完,就先执行吗?假如a.js很大,jquery.js先下载完,那么是不是浏览器先执行jquery.js?谢谢

@army8735
Copy link
Member

army8735 commented Aug 7, 2014

先执行定义,真正执行是你程序的逻辑。

@24wangchen
Copy link

mark

@hkongm
Copy link

hkongm commented Sep 17, 2014

膜拜下。

@kunogi
Copy link

kunogi commented Sep 17, 2014

今天才看到这篇强文 马克土温一下

@webfing
Copy link

webfing commented Oct 14, 2014

看来是需要反复阅读才能很好得理解

@tsangint
Copy link

mark

@chaojidan7252
Copy link

先编译a.js, 然后会执行a.js中function,这时因为依赖b.js就会去执行b.js中的function, 那我想问jquery.js是在a,b,c都执行完成后,再编译,执行function吗?

@Go7hic
Copy link

Go7hic commented Dec 5, 2014

学习

@linchen1987
Copy link

正在看seajs源码,一头雾水。 竟然找到这篇文章,真棒! 支持开源社区

@frankxin
Copy link

frankxin commented Aug 8, 2015

mark~学习了

@dubinbin
Copy link

厉害,还是很多看不懂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests