珠峰培训

seajs实用教程(四) 内部执行过程

作者:

2015-11-23 15:35:17

99

在详细的讨论下 Module_ID的解析规则

Sea.js 中的模块标识是 CommonJS 模块标识 的超集:

  • 一个模块标识由斜线(/)分隔的多项组成。
  • 每一项必须是小驼峰字符串、 . 或 .. 。
  • 模块标识可以不包含文件后缀名,比如 .js 。
  • 模块标识可以是 相对 或 顶级 标识。如果第一项是 . 或 ..,则该模块标识是相对标识。
  • 顶级标识根据模块系统的基础路径来解析。
  • 相对标识相对 require 所在模块的路径来解析。

注意,符合上述规范的标识肯定是 Sea.js 的模块标识,但 Sea.js 能识别的模块标识不需要完全符合以上规范。 比如,除了大小写字母组成的小驼峰字符串,Sea.js 的模块标识字符串还可以包含下划线(_)和连字符(-), 甚至可以是 http://、https://、file:/// 等协议开头的绝对路径。

相对标识

相对标识以 . 开头,只出现在模块环境中(define 的 factory 方法里面)。相对标识永远相对当前模块的 URI 来解析:

// 在 http://example.com/js/a.js 的 factory 中:
require.resolve('./b');
  // => http://example.com/js/b.js

// 在 http://example.com/js/a.js 的 factory 中:
require.resolve('../c');
  // => http://example.com/c.js
顶级标识

顶级标识不以点(.)或斜线(/)开始, 会相对模块系统的基础路径(即 Sea.js 的 base 路径)来解析:

// 假设 base 路径是:http://example.com/assets/

// 在模块代码里:
require.resolve('gallery/jquery/1.9.1/jquery');
  // => http://example.com/assets/gallery/jquery/1.9.1/jquery.js

模块系统的基础路径即 base 的默认值,与 sea.js 的访问路径相关:

如果 sea.js 的访问路径是:
  http://example.com/assets/sea.js

则 base 路径为:
  http://example.com/assets/
当 sea.js 的访问路径中含有版本号时,base 不会包含 seajs/x.y.z 字串。 当 sea.js 有多个版本时,这样会很方便。

如果 sea.js 的路径是:
  http://example.com/assets/seajs/1.0.0/sea.js

则 base 路径是:
  http://example.com/assets/
当然,也可以手工配置 base 路径:

seajs.config({
  base: 'http://code.jquery.com/'
});

// 在模块代码里:
require.resolve('jquery');
  // => http://code.jquery.com/jquery.js

普通路径

除了相对和顶级标识之外的标识都是普通路径。普通路径的解析规则,和 HTML 代码中的 一样,会相对当前页面解析。

// 假设当前页面是 http://example.com/path/to/page/index.html

// 绝对路径是普通路径:
require.resolve('http://cdn.com/js/a');
  // => http://cdn.com/js/a.js

// 根路径是普通路径:
require.resolve('/js/b');
  // => http://example.com/js/b.js

// use 中的相对路径始终是普通路径:
seajs.use('./c');
  // => 加载的是 http://example.com/path/to/page/c.js

seajs.use('../d');
  // => 加载的是 http://example.com/path/to/d.js

提示:

顶级标识始终相对 base 基础路径解析。 绝对路径和根路径始终相对当前页面解析。 require 和 require.async 中的相对路径相对当前模块路径来解析。 seajs.use 中的相对路径始终相对当前页面来解析。 文件后缀的自动添加规则

Sea.js 在解析模块标识时, 除非在路径中有问号(?)或最后一个字符是井号(#),否则都会自动添加 JS 扩展名(.js)。如果不想自动添加扩展名,可以在路径末尾加上井号(#)。

// ".js" 后缀可以省略:
require.resolve('http://example.com/js/a');
require.resolve('http://example.com/js/a.js');
  // => http://example.com/js/a.js

// ".css" 后缀不可省略:
require.resolve('http://example.com/css/a.css');
  // => http://example.com/css/a.css

// 当路径中有问号("?")时,不会自动添加后缀:
require.resolve('http://example.com/js/a.json?callback=define');
  // => http://example.com/js/a.json?callback=define

// 当路径以井号("#")结尾时,不会自动添加后缀,且在解析时,会自动去掉井号:
require.resolve('http://example.com/js/a.json#');
  // => http://example.com/js/a.json

设计原则

模块标识的规则就上面这些,设计的核心出发点是:

关注度分离。比如书写模块 a.js 时,如果需要引用 b.js,则只需要知道 b.js 相对 a.js 的相对路径即可,无需关注其他。

尽量与浏览器的解析规则一致。比如根路径(/xx/zz)、绝对路径、以及传给 use 方法的非顶级标识,都是相对所在页面的 URL 进行解析。

这里分析的版本是 1.3.0

先看看模块加载的整体思路

  • 1、从seajs.use方法入口,开始加载use到的模块。
  • 2、use到的模块这时mod缓存当中一定是不存在的。seajs创建一个新的mod,赋予一些初始的状态。
  • 3、执行mod.load方法
  • 4、一堆逻辑之后走到seajs.request方法,请求模块文件。模块加载完成之后,执行define方法。
  • 5、define方法分析提取模块的依赖模块,保存起来。缓存factory但不执行。
  • 6、模块的依赖模块再被加载,如果继续有依赖模块,则继续加载。直至所有被依赖的模块都加载完毕。
  • 7、所有的模块加载完毕之后,执行use方法的callback.
  • 8、模块内部逻辑从callback开始执行。require方法在这个过程当中才被执行。

1 从线团的线头抓起,从use说起

seajs.use方法有两个参数,第一个参数是要加载的模块,第二个是加载完模块后的回调函数(可选)

其中要加载的模块,可以为一个字符串,也可以为一个数组。

譬如:

seajs.use("a",function(){})

seajs.use(['a','b'],function(){})

1 看看use里边都做了些什么?

function preload(callback) {
    var preloadMods = config.preload.slice()
    config.preload = []
    preloadMods.length ? globalModule._use(preloadMods, callback) : callback()
}


// Public API
// ----------

var globalModule = new Module(util.pageUri, STATUS.COMPILED)

seajs.use = function(ids, callback) {
    // Loads preload modules before all other modules.
    preload(function() {
        globalModule._use(ids, callback)
    })

    // Chain
    return seajs
}

 这段代码里在遇到seajs.use时候先加载了在config中定义的preload的模块,再去加载use里边的需要加载的模块。

2 下来我们再看 globalModule.use(ids, callback) 也就到了module.prototypeuse方法了。

Module.prototype._use = function(ids, callback) {
    util.isString(ids) && (ids = [ids])
    var uris = resolve(ids, this.uri)

    this._load(uris, function() {
        // Loads preload files introduced in modules before compiling.
        preload(function() {
            var args = util.map(uris, function(uri) {
                return uri ? cachedModules[uri]._compile() : null
            })

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

先将单个ids转成数组,使用resolve获取到ids的uris,下来继续使用module.prototype._load()去加载模块,在加载完成后再去compile()分析模块,所有模块都加载了,再去执行use函数的callback方法。

3 分析module.prototype._load()

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

     //获取到还没有加载过的资源列表
    var unLoadedUris = util.filter(uris, function(uri) {
        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) {
            var module = cachedModules[uri] ||
                (cachedModules[uri] = new Module(uri, STATUS.FETCHING))

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

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

                if (module.status >= STATUS.SAVED) {
                     //模块加载完成后,查看是否有其它依赖模块,有其它依赖模块则继续加载新的模块,所有的文件都加载完成后,执行回调函数
                    var deps = getPureDependencies(module)

                    if (deps.length) {
                        Module.prototype._load(deps, function() {
                            cb(module)
                        })
                    } else {
                        cb(module)
                    }
                }
                // Maybe failed to fetch successfully, such as 404 or non-module.
                // In these cases, just call cb function directly.
                else {

                    cb()
                }
            }

        })(unLoadedUris[i])
    }

    function cb(module) {
        (module || {}).status < STATUS.READY && (module.status = STATUS.READY)
            --remain === 0 && callback()
    }
}

4 在这里看下如何分析依赖模块的 getPureDependencies(module)

在这里需要注意的是模块里边的循环调用处理。

 function getPureDependencies(module) {
    var uri = module.uri

     //循环调用的直接去除了,在加载

    return util.filter(module.dependencies, function(dep) {
        circularCheckStack = [uri]

        var isCircular = isCircularWaiting(cachedModules[dep])
        if (isCircular) {
            circularCheckStack.push(uri)
            printCircularLog(circularCheckStack)
        }

        return !isCircular
    })
}

function isCircularWaiting(module) {
    if (!module || module.status !== STATUS.SAVED) {
        return false
    }

    circularCheckStack.push(module.uri)
    var deps = module.dependencies

    if (deps.length) {
        if (isOverlap(deps, circularCheckStack)) {
            return true
        }

        for (var i = 0; i < deps.length; i++) {
            if (isCircularWaiting(cachedModules[deps[i]])) {
                return true
            }
        }
    }

    circularCheckStack.pop()
    return false
}

function printCircularLog(stack, type) {
    util.log('Found circular dependencies:', stack.join(' --> '), type)
}

function isOverlap(arrA, arrB) {
    var arrC = arrA.concat(arrB)
    return arrC.length > util.unique(arrC).length
}

再看看在模块定义define中都做了些什么?

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

    // define(factory)
    if (argsLength === 1) {
        factory = id
        id = undefined
    }
    // define(id || deps, factory)
    else if (argsLength === 2) {
        factory = deps
        deps = undefined

        // define(deps, factory)
        if (util.isArray(id)) {
            deps = id
            id = undefined
        }
    }

    // Parses dependencies.
    if (!util.isArray(deps) && util.isFunction(factory)) {
            //parseDependencies方法做的事情主要就是用一个正则表达式把函数体里面所有require(XXX)里面的XXX提取出来,这也就是这个函数依赖到的所有模块了。
        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.
        var script = util.getCurrentScript()
        if (script) {
            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) {
        // For IE:
        // If the first module in a package is not the cachedModules[derivedUri]
        // self, it should assign to the correct module when found.
        if (resolvedUri === derivedUri) {
            var refModule = cachedModules[derivedUri]
            if (refModule && refModule.realUri &&
                refModule.status === STATUS.SAVED) {
                cachedModules[derivedUri] = null
            }
        }

        var module = Module._save(resolvedUri, meta)

        // For IE:
        // Assigns the first module in package to cachedModules[derivedUrl]
        if (derivedUri) {
            // cachedModules[derivedUri] may be undefined in combo case.
            if ((cachedModules[derivedUri] || {}).status === STATUS.FETCHING) {
                cachedModules[derivedUri] = module
                module.realUri = derivedUri
            }
        } else {
            firstModuleInPackage || (firstModuleInPackage = module)
        }
    } else {
        // Saves information for "memoizing" work in the onload event.
        anonymousModuleMeta = meta
    }
}


var REQUIRE_RE = /(?:^|[^.$])\brequire\s*\(\s*(["'])([^"'\s\)]+)\1\s*\)/g


 分析完deps之后,将模块定义存入缓存:
util.parseDependencies = function(code) {
    // Parse these `requires`:
    //   var a = require('a');
    //   someMethod(require('b'));
    //   require('c');
    //   ...
    // Doesn't parse:
    //   someInstance.require(...);
    var ret = [],
        match

     //去除注释
    code = removeComments(code)
    REQUIRE_RE.lastIndex = 0

    while ((match = REQUIRE_RE.exec(code))) {
        if (match[2]) {
            ret.push(match[2])
        }
    }

    return util.unique(ret)
}

下面说举几个例子说下:

index.html:

<script src="lib/seajs/seajs1.3.0.js"></script>

<script>
    seajs.config({
        base:"./js/"
    })
    seajs.use(["a"],function(){
        console.log("a.js and b.js saved");
    })
</script>

a.js:

define(function(require,exports,module){
    var b = require("b");

    console.log("a.js exec");

    console.log(module);
})

b.js:

define(function(require,exports,module){
    var b = require("a");

    console.log("b.js exec");

    console.log(module);

    var c = require.async("c");

})

c.js:

define(function(require,exports,module){
    console.log("c.js exec");
    console.log(module);    
})

执行结果如下:

上述例子seajs模块加载的逻辑,如下图:

a依赖b b依赖a和c c不依赖

尽管存在上述依赖,但是a,b,c,d模块download到浏览器端的顺序确是a,b,c而不是c,b,a,笨想一下后一种执行顺序也是不可能的,因为模块间的依赖只有download到浏览器端seajs才能进行分析。

概括一下整个加载的流程就是:

自顶向下的download,自底向上的反馈准备就绪。

如何做到的呢?

主要是Module中的几个属性发挥的作用,模块被download到浏览器端后,按照CMD规范,define函数会被执行,module.define会分析该模块的依赖,记录到dependencies属性中,define函数执行完毕,绑定在script标签上的onload事件会被触发,进而加载当前模块的依赖模块,也就是执行module.load函数,这是一个循环往复的过程。

假设d模块加载就绪,执行module.load时发现,d模块已无其他依赖,进而执行module.onload, 在module.onload中,通过waitings属性找到父模块,操作父模块的依赖计数remain,达到通知父模块的目的。

这是一个完美的反馈系统。

至此,模块加载过程就算是说完了。

最后请大家再看下这章代码结构梳理图片

图片来源:http://www.cnblogs.com/nuysoft/archive/2012/07/27/2610971.html