基于AMD的模块加载个人总结

本文主要希望能够解释一些相关的东西,而非面面俱到,也只是个人对于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;
        }
    }
}

个人思考的总结,供大家参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值