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