本文主要希望能够解释一些相关的东西,而非面面俱到,也只是个人对于AMD模块加载机制的总结。
只关注以下几个问题:
一、什么叫AMD
AMD —— 'Asynchronous Module Definition'缩写,解释为异步模块定义。
二、为什么要AMD
对于前端脚本而言,我们主要关注于代码的可扩展性和可复用性以及可维护性,页面加载中如果同步会导致加载过程过长页面假死的现象。当所有代码不再是散乱的script而是可供管理的,可按需加载的模块时,以上的性能才可从谈起。
三、闭包相对于AMD的劣势
闭包从某种程度上来说也是一种封装,但是在没有统一异步的机制做管理的时候,还是一个个脚本同步加载。闭包并非所谓在构建前端过程中毫无建树,除了在局部使用闭包对某些不希望被修改的变量有极大用处外,也在全局框架中一定程度上控制了命名空间的污染。闭包的其他特性,作用域等不在此处赘述。
四、CMD和AMD主要区别是什么?
关于这个,玉伯曾做过比较详细的说明,主要有以下几点
1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
2. CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
...
})
虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
五、AMD机制的主要接口
1.require —— require(["a","b"], function(a, b) {})
2.define —— define('xxx', ["a","b"],function(a,b){})
六、异步加载机制探知所在路径
function getBasePath() {
var nodes = document.getElementsByTagName('script');
var node = nodes[nodes.length - 1];
var src = document.querySelector? node.src : node.getAttribute("src", 4);
return src;
}
这是一个非常初级的实现,但是在底版本的IE就会出错,所以必须要做一些浏览器兼容
// 如果是IE浏览器,判断的方法可自选
for (var i = 0, node; node = nodes[i ++] {
if (node.readyState === "interactive") {
break;
} else {
node = nodes[nodes.length - 1];
}
})
至此需要考虑是否还能优化,参考司徒正美的解决方案,他的方法用script代码取代访问DOM
try {
a.b.c()
} catch (e) {
if (e.fileName) { //firefox
return e.fileName;
} else if (e.sourceURL) { //safari
return e.sourceURL;
}
}
这里,他利用了Error对象,很好的想法。
七、require接口
1. deps ID 至 URL 的转换
2. 检测当前模块的加载情况
3. 创建script node,绑定事件,并插入DOM tree
4. 保留所有模块的URL和deps,在加载事件触发时check
如lofty一样,存在别名机制,lofty代码:
fmd( 'alias', ['config','event'],
function( config, event ){
'use strict';
var ALIAS = 'alias';
config.register({
keys: ALIAS,
name: 'object'
});
event.on( ALIAS, function( meta ){
var aliases = config.get( ALIAS ),
alias;
if ( aliases && ( alias = aliases[meta.id] ) ){
meta.id = alias;
}
} );
} );
但是对于不符合AMD定义的文件就需要shim机制了,
requireJS中,require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。
比如,jQuery的插件可以这样定义:
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
源码中真正实施require核心的是在context.require方法里面
这里不详细说明validation的逻辑,就个人总结下注意的地方
一般会有如下三个方法,或者你可以实现这三个功能而不做单独方法
ID2URL: 将ID转化成URL,这个方法里面需要实现shim的解析,然后这个方法是load CSS 和 JS的入口
Handler: 执行用户回调,最终目的
checkLoaded: 检测安装的情况
这里有个逻辑可参考,无论是正美的框架还是requireJS,都是检测是否已经安装(在注册列表中requireJS,需要安装的模块数和已经安装完的模块数mass,如果是就执行回调,否则会创建一个对象,记录模块的加载情况和其他信息,然后放入检测队列。
至于checkLoaded 一般会在script.onload的前后都有一次执行
PS:requireJS源码在 load JS的方法里面有一个判断分支
else if (isWebWorker) {
try {
importScripts(url);
context.completeLoad(moduleName);
} catch (e) {
context.onError(makeError('importscripts',
'importScripts failed for ' +
moduleName + ' at ' + url,
e,
[moduleName]));
}
}
我觉得这个很让人兴奋~~~
八、define接口
1.id,在正美的框架中,这个只是一个给开发者看的摆设,主要还是通过getCurrentScript方法获取,但是requireJS中却不然。
2.deps,如果缺失,在mass和requireJS都会补上一个空数组,这里有一个define最重要的也可也以说是唯一的难点:循环依赖
3.callback,用户回调handler
requireJS中的循环依赖检测
function breakCycle(mod, traced, processed) {
var id = mod.map.id;
if (mod.error) {
mod.emit('error', mod.error);
} else {
traced[id] = true;
each(mod.depMaps, function (depMap, i) {
var depId = depMap.id,
dep = getOwn(registry, depId);
//Only force things that have not completed
//being defined, so still in the registry,
//and only if it has not been matched up
//in the module already.
if (dep && !mod.depMatched[i] && !processed[depId]) {
if (getOwn(traced, depId)) {
mod.defineDep(i, defined[depId]);
mod.check(); //pass false?
} else {
breakCycle(dep, traced, processed);
}
}
});
processed[id] = true;
}
}
defineDep: function (i, depExports) {
//Because of cycles, defined callback for a given
//export can be called more than once.
if (!this.depMatched[i]) {
this.depMatched[i] = true;
this.depCount -= 1;
this.depExports[i] = depExports;
}
},
逻辑主要:递归的检测,如果状态为已安装而且所有依赖的递归中有存在id与之相同的,则视为循环依赖,在其他在其之后加载的循环依赖的该模块所获得的值为undefined
正美给出的代码较容易理解
function checkCycle(deps, nick){
for (var id in deps) {
if (deps[id] === "司徒正美" && modules[id].state !== 2 && (id === nick || checkCycle(modules[id].deps , nick))) {
return true;
}
}
}
个人思考的总结,供大家参考。