AMD加载器实现笔记(一)

  之前研究过AMD,也写过一篇关于AMD的文章《以代码爱好者角度来看AMD与CMD》。代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的。因为今年计划的dojo教程《静静的dojo》中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下。时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领。作为开发人员,深感惭愧。故有此文,记录我在实现一个AMD加载器时的思考总结。

  requireJS是所有AMD加载器中,最广为人知的一个。目前的版本更凝聚了几位大牛数年心血,必然不是我这个小虾米一晚上的粗制滥造能够比拟的,所以目前为止这篇文章里的加载器尚不能称为AMD加载器。它并不支持AMD规范中对config的配置项,甚至不支持在define中明确地声明模块Id,而且它现在只支持chrome浏览器。它的API如下:

require([
  ‘http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb‘,
  ‘http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc‘,
  ‘http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc‘,
  ‘http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd‘,
  ‘http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff‘], function(aaabbbccc){
    console.log(‘simple loader‘);
    console.log(arguments);
  });
define(["http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
    $.log("已加载ccc模块")
    return {
        aaa: aaa,
        ccc: "ccc555"
    }
})

  是的,目前并不支持模块解析功能,所以模块id只能是绝对路径。但对于一个简易的加载器已经足够,因为它还将会被迭代。

  

  既然AMD是JavaScript模块化的解决方案,解决不支持模块化的JavaScript,那么任何一个解决方案都有必要在概念层面上去定义模块。在这里模块的定义是,使用define函数包装的js文件。既然是文件那首要解决加载的问题,异步无阻塞的的加载方式有多种解决方案,但最终被开发者广泛认可的是动态创建script标签的方式(不明白的同学去看一下这篇文章探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密)。

function loadJS(url) {
        var script = document.createElement(‘script‘);
        script.type = "text/javascript";
        script.src = url + ‘.js‘;
        script.onload = function() {
            //干你的活
        };
        var head = document.getElementsByTagName(‘head‘)[0];
        head.appendChild(script);
    };    

  文件加载完毕后,会立即执行define函数。define函数包装后的模块在加载器内部的数据结构如下:

  module:

  • id: 模块的唯一标识
  • deps:模块依赖项的标识数组
  • factory:依赖项全部执行完毕后所执行的函数,所有模块的代码都写在这个函数里
  • export:模块代码执行完毕后的输出对象
  • state:模块的状态(AMD是要解决JavaScript模块依赖的问题,所以一个模块需要等待所有依赖项完成后才能执行模块的factory函数。我们需要state属性标识模块的状态,注册为1,执行完毕为2.)

  

  我们先从define函数开始。

global.define = function(deps, callback) {
        var id = getCurrentScript();
        if (modules[id]) {
            console.error(‘multiple define module: ‘ + id);
        }

        require(deps, callback, id);
    };

function getCurrentScript(base) {
        // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
        var stack;
        try {
            a.b.c(); //强制报错,以便捕获e.stack
        } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
            stack = e.stack;
            if (!stack && window.opera) {
                //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
                stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
            }
        }
        if (stack) {
            /**e.stack最后一行在所有支持的浏览器大致如下:
             *chrome23:
             * at http://113.93.50.63/data.js:4:1
             *firefox17:
             *@http://113.93.50.63/query.js:4
             *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
             *@http://113.93.50.63/data.js:4
             *IE10:
             *  at Global code (http://113.93.50.63/data.js:4:1)
             *  //firefox4+ 可以用document.currentScript
             */
            stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
            stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
            return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
        }
        var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
        for (var i = nodes.length, node; node = nodes[--i]; ) {
            if ((base || node.className === moduleClass) && node.readyState === "interactive") {
                return node.className = node.src;
            }
        }
    };

getCurrentScript

  我们的define仅支持匿名模块,所以第一件事便是需要一个模块id。根据这个id我们需要能够找出对应的Js文件。这里我们利用了Chrome的ReferenceError实例的stack属性。强制浏览器报错,获取error的stack属性,通过正则表达式匹配出文件的绝对路径。 依赖的模块的加载只需加载一次即可,禁止多次加载,所以遇到重复加载情况需要报错。注册模块与加载依赖项的工作交给了require函数来处理。

  require函数是这里的大头,接下来我们便去揭开它的神秘面纱。

//module: id, state, factory, result, deps;
    global.require = function(deps, callback, parent){
        var id = parent || "Bodhi" + Date.now();
        var cn = 0, dn = deps.length;
        var args = [];

        var module = {
            id: id,
            deps: deps,
            factory: callback,
            state: 1,
            result: null
        };
        modules[id] = module;

        deps.forEach(function(dep) {
            if (modules[dep] && modules[dep].state === 2) {
                cn++
                args.push(modules[dep].result);
            } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
                loadJS(dep);
                loadedJs.push(dep);
            }
        });
        if (cn === dn) {
            callFactory(module);
        } else {
            //loadJS(id);// require只是用来加载其他模块的
            loadings.push(id);
            checkDeps();
        }
    };

  因为define将责任推给了require,所以require的首要任务便是注册模块。JavaScript对于hash结构有着原生的支持,原生的对象{}做模块仓库最适合不过了。

  接下来就是处理依赖项,如果模块的依赖项并未被加载,那就去加载它;另外记录下已加载的依赖模块数量。

  如果依赖模块被执行完毕,那就去执行模块的factory函数;如果依赖项没有执行完毕,那就把模块id放入加载队列中,并执行依赖检查。

  加载模块的工作交给了loadJs函数:

function loadJS(url) {
        var script = document.createElement(‘script‘);
        script.type = "text/javascript";
        script.src = url + ‘.js‘;
        script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };
        var head = document.getElementsByTagName(‘head‘)[0];
        head.appendChild(script);
    };

  无论模块的依赖关系是多么复杂,当所有的依赖关系被确定后,必然有一个最后被等待的模块。这就好比武侠小说中,每个杀阵都有阵眼,只要破去阵眼就能破阵。我们称这最后被等待的模块为阵眼模块。当阵眼模块被执行完毕后,整个依赖网便被盘活,一层层的回归似的,执行factory函数。

  而如何判断一个模块是阵眼模块呢?我们以deps为0作为依据。放在isRedy函数中。

function isReady(m) {
        var deps = m.deps;
        var allReady = deps.every(function(dep) {
            return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
        })
        if (deps.length === 0 || allReady) {
            return true;
        }
    };

  而盘活的契机放在script的onload函数中。一个script元素的生命周期为:

  创建元素-》加载脚本文件-》解析脚本文件(执行js代码)-》onload事件-》销毁

  所以如果onload中模块是阵眼模块,或者依赖模块已被全部加载完毕,则执行factory函数。然后循环检查依赖,一层一层的盘活其他依赖网。

script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };

  整个加载器代码如下:

(function(global){
    global.$ = {
        log: function(m) {
            console.log(m);
        }
    };
    global = global || window;
    modules = {};
    loadings = [];
    loadedJs = [];
    //module: id, state, factory, result, deps;
    global.require = function(deps, callback, parent){
        var id = parent || "Bodhi" + Date.now();
        var cn = 0, dn = deps.length;
        var args = [];

        var module = {
            id: id,
            deps: deps,
            factory: callback,
            state: 1,
            result: null
        };
        modules[id] = module;

        deps.forEach(function(dep) {
            if (modules[dep] && modules[dep].state === 2) {
                cn++
                args.push(modules[dep].result);
            } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
                loadJS(dep);
                loadedJs.push(dep);
            }
        });
        if (cn === dn) {
            callFactory(module);
        } else {
            //loadJS(id);// require只是用来加载其他模块的
            loadings.push(id);
            checkDeps();
        }
    };

    global.define = function(deps, callback) {
        var id = getCurrentScript();
        if (modules[id]) {
            console.error(‘multiple define module: ‘ + id);
        }

        require(deps, callback, id);
    };

    function loadJS(url) {
        var script = document.createElement(‘script‘);
        script.type = "text/javascript";
        script.src = url + ‘.js‘;
        script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };
        var head = document.getElementsByTagName(‘head‘)[0];
        head.appendChild(script);
    };

    function checkDeps() {
        for (var p in modules) {
            var module = modules[p];
            if (isReady(module) && loadings.indexOf(module.id) > -1) {
                callFactory(module);
                checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功
            }
        }
    };

    function isReady(m) {
        var deps = m.deps;
        var allReady = deps.every(function(dep) {
            return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
        })
        if (deps.length === 0 || allReady) {
            return true;
        }
    };

    function callFactory(m) {
        var args = [];
        for (var i = 0, len = m.deps.length; i < len; i++) {
            args.push(modules[m.deps[i]].result);
        }
        m.result = m.factory.apply(window, args);
        m.state = 2;

        var idx = loadings.indexOf(m.id);
        if (idx > -1) {
            loadings.splice(idx, 1);
        }
    };

    function getCurrentScript(base) {
        // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
        var stack;
        try {
            a.b.c(); //强制报错,以便捕获e.stack
        } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
            stack = e.stack;
            if (!stack && window.opera) {
                //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
                stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
            }
        }
        if (stack) {
            /**e.stack最后一行在所有支持的浏览器大致如下:
             *chrome23:
             * at http://113.93.50.63/data.js:4:1
             *firefox17:
             *@http://113.93.50.63/query.js:4
             *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
             *@http://113.93.50.63/data.js:4
             *IE10:
             *  at Global code (http://113.93.50.63/data.js:4:1)
             *  //firefox4+ 可以用document.currentScript
             */
            stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
            stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
            return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
        }
        var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
        for (var i = nodes.length, node; node = nodes[--i]; ) {
            if ((base || node.className === moduleClass) && node.readyState === "interactive") {
                return node.className = node.src;
            }
        }
    };
})(window)

  测试代码:

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
    <title>Web AppBuilder for ArcGIS</title>
    <link rel="shortcut icon" href="builder/images/shortcut.png">
  </head>
  <body class="claro">
  <script src="./loader.js"></script>
  <script>
  require([
  ‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb‘,
  ‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc‘,
  ‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc‘,
  ‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd‘,
  ‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff‘], function(aaabbbccc){
    console.log(‘simple loader‘);
    console.log(arguments);
  });
  </script>
  </body>
</html>

define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc"],function(a, c){
    console.log("已加载bbb模块", 7)
    return {
        aaa: a,
        ccc: c.ccc,
        bbb: "bbb"
    }
})

bbb

define([], function(){
    console.log("已加载aaa.bbb.ccc模块", 7)
    return "aaa.bbb.ccc";
});

aaa.bbb.ccc

define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
    $.log("已加载ccc模块")
    return {
        aaa: aaa,
        ccc: "ccc555"
    }
})

ccc

define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff"],function(a,b,c,f){
    $.log("已加载ddd模块", 7);
    return {
        bbb: b,
        ddd: "ddd",
        length: arguments.length
    }
})

ddd

define([‘http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg‘], function(g){
    $.log("已加载fff模块")
    return {
        ggg: g,
        fff: "fff"
    }
})

fff

define([], function(){
    console.log("已加载aaa模块", 7)
    return "aaa"
});

aaa

define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg"],function(ret){
    $.log("已加载eee模块",7)
    return {
        eee: "eee",
        aaa: ret.aaa,
        ggg: ret.ggg
    }
})

eee

define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(a){
    $.log("已加载ggg模块",7)
    return {
        aaa: a,
        ggg:"ggg"
    }
})

ggg

  执行结果如下:

已加载aaa模块 7
loader.js:4 已加载ggg模块
loader.js:4 已加载fff模块
aaa.bbb.ccc.js:2 已加载aaa.bbb.ccc模块 7
loader.js:4 已加载ccc模块
bbb.js:3 已加载bbb模块 7
loader.js:4 已加载ddd模块
index.html:19 simple loader
index.html:20 Arguments[5]

  下一篇文章将会为我们的加载器加上模块路径解析功能,到时候我们便不用书写如此丑陋的模块id了。

  

  如果您觉得这篇文章对您有帮助,请不吝点击右下方推荐~

时间: 2024-10-10 00:44:09

AMD加载器实现笔记(一)的相关文章

AMD加载器实现笔记(二)

AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器.但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置.这篇文章中我们来添加对config的中baseUrl和packages的支持.API设计如下: 1 require.config({ 2 baseUrl: "./", 3 packages: [{ 4 name: "more", 5 location: "./more" 6 }, {

AMD加载器实现笔记(五)

前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖中有没有依赖A模块本身的模块.如果有那就说明存在环形依赖.所以检验的方式是利用递归,检查一个模块的依赖的依赖项中有没有依赖A模块,以及依赖项的依赖项的依赖项中有没有A模块,核心代码如下: function checkCircleRef(start, target){ var m = modules[

AMD加载器实现笔记(四)

继续这一系列的内容,到目前为止除了AMD规范中config的map.config参数外,我们已经全部支持其他属性了.这一篇文章中,我们来为增加对map的支持.同样问题,想要增加map的支持首先要知道map的语义. 主要用于解决在两个不同模块集中使用一个模块的不同版本,并且保证两个模块集的交互没有冲突. 假设磁盘有如下文件: 当'some/newmodule'请求'foo'模块时,它将从foo1.2.js总得到'foo1.2'模块:当'some/oldmodule'请求'foo'模块时它将从foo

AMD加载器实现笔记(三)

上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持. 要添加shim与paths,第一要务当然是了解他们的语义与用法.先来看shim,shim翻译成中文是“垫片”的意思.在AMD中主要用途是把不支持AMD的某些变量包装AMD模块.shim是一个哈希对象,key为包装后的模块Id,value是关于这个包装模块的一些配置,主要配置项如下: deps:定义模块需要的依赖项的moduleId数组 exports:模块输出值 in

JavaScript AMD 模块加载器原理与实现

关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者遵循 CMD规范.前者的规范产出比较适合于浏览器异步环境的习惯,后者的规范产出对于写过 nodejs 的同学来说是比较爽的.关于两者的比较,有兴趣的同学请参看玉伯在知乎的回答 AMD和CMD的区别有哪些.本文希望能按照 AMD 规范来简单实现自己的一个模块加载器,以此来搞清楚模块加载器的工作原理.

浏览器端的javascript加载器

commonJS,定义了一种同步加载脚本的规范.对于浏览器端而言,因为js脚本都是在远端,用同步的方式可能会长时间阻塞线程.因此,浏览器端的js加载器并不会严格按照commonJS来做.seajs作为一个试图遵循commonJS规范的加载器,是在规范的基础上在外面包一层define,来异步加载js,以下是seajs官网的一个例子: // 所有模块都通过 define 来定义 define(function(require, exports, module) { // 通过 require 引入依

【模块化编程】理解requireJS-实现一个简单的模块加载器

在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ...... 为了深入了解加载器,中间阅读过一点requireJS的源码,但对于很多同学来说,对加载器的实现依旧不太清楚 事实上不通过代码实现,单单凭阅读想理解一个库或者框架只能达到一知半解的地步,所以今天便来实现一个简单的加载器 加载器原理分析 分与合 事实上,一个程序运行需要完整的模块,以下代码为例: 1

《你必须知道的.NET》读书实践:一个基于OO的万能加载器的实现

此篇已收录至<你必须知道的.Net>读书笔记目录贴,点击访问该目录可以获取更多内容. 一.关于万能加载器 简而言之,就是孝顺的小王想开发一个万能程序,可以一键式打开常见的计算机资料,例如:文档.图片和影音文件等,只需要安装一个程序就可以免去其他应用文件的管理(你让其他耗费了巨资打造的软件情何以堪...),于是就有了这个万能加载器(FileLoader). 初步分析之后,小王总结了这个万能加载器的功能点如下: (1)能够打开常见文档类资料:txt.word.pdf.visio等: (2)能够打开

JS模块加载器加载原理是怎么样的?

路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的对应原则,加载器才能知道需要加载的 js 的路径.在这个例子里,就是 baseUrl + 'a.js' 和 baseUrl + 'b.js'. 但 id 和 path 的对应关系并不是永远那么简单,比如在 AMD 规范里就可以通过配置 Paths 来给特定的 id 指配 path. 原理二:crea