requirejs 源码解析

vscode 源码使用 vscode-loader 加载模块,vscode-loader 是异步模块定义 (AMD) 加载器的一种实现。而 AMD 规范的实现典范是 requirejs,可在浏览器、node 等环境中,异步加载 js 或模块。

本文先学习梳理 requirejs 的源码,了解 AMD 一般是如何实现的。在后面的文章中,再进一步学习 vscode-loader 的实现。

requirejs 的使用示例

require.js 在 浏览器中的使用方法如下:

html: 标签的 src 指定为 require.js,data-main 指定为入口 js。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <!-- 指定入口脚本 a/b.js -->
    <script src="require.js" data-main="a/b.js" ></script>
</head>
<body></body>
</html>

入口 js:调用 requirejs 方法,加载模块。

// a/b.js

// 配置模块路径
require.config({
    paths: {
        test: 'test'
    }
});

// 加载模块
requirejs(['test'], function(test) {
    test.compare(2, 5)
});

定义模块:

// test.js

define('test', function() {
    return {
        compare: function(a, b) {
            return a > b;
        }
    }
});

define 方法:define(id?, dependencies?, factory);,第一个参数是模块名,第二个参数是依赖,第三个参数是模块的工厂函数,返回定义的模块。

requirejs 的代码解析

requirejs 的主体是一个自执行函数。下面代码省略了许多细节,先看一下基本的结构。

var requirejs, require, define;
(function (global, setTimeout) {
    // 定义一系列的变量和函数,最主要是 newContext、req、define
    function newContext(contextName) {}
    req = requirejs = function (deps, callback, errback, optional) {}
    define = function (name, deps, callback) {}
	
    // 第一步:创建默认上下文
    req({});

    // 第二步:浏览器环境,查找入口 js,放到配置中
    if (isBrowser && !cfg.skipDataMain) {
        ...

        cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];

        ...
    }

    // 第三步:根据配置,加载入口 js
    req(cfg);
    
}(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));

可以看到,requirejs 可以分为三步:创建默认上下文、查找入口 js、加载入口 js。

第一步和第三步都是调用 req 函数,涉及代码较多。我们先看看相对简单的第二步,查找入口 js 的具体实现。

查找入口 js

从前面的示例 <script src="require.js" data-main="a/b.js" ></script>,我们知道,入口 js 在 script 标签的 data-main 属性中指定。所以查找入口 js,也就是先找到 data-main,再进行解析。具体如下:


    if (isBrowser && !cfg.skipDataMain) {
        // 第一步:遍历 script 标签
        // 注:eachReverse,对 scripts 数组,遍历执行传入的函数,函数返回值为 true 时中止遍历
        eachReverse(scripts(), function (script) {
            // 第二步:保存 script 标签的父元素
            if (!head) {
                head = script.parentNode;
            }

            // 第三步:获取 data-main 属性
            dataMain = script.getAttribute('data-main');

            // 第四步:解析 data-main,获取入口 js,放到配置中
            if (dataMain) {
                // 略,在下面展开讨论
                ...
            }
        });
    }

    // 相关函数如下:
    /**
     * 遍历数组,执行函数,函数返回 true 时,break
     */
    function eachReverse(ary, func) {
        if (ary) {
            var i;
            for (i = ary.length - 1; i > -1; i -= 1) {
                if (ary[i] && func(ary[i], i, ary)) {
                    break;
                }
            }
        }
    }

    function scripts() {
        return document.getElementsByTagName('script');
    }

解析 data-main 的具体逻辑如下:


if (dataMain) {
    mainScript = dataMain;

    // 第一步:如果没有 baseUrl,则解析获取 baseUrl
    // 注:mainScript.indexOf('!') === -1,该判断是指 data-main 值不是加载器插件模块的 ID。
    if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {

        // 1. data-main 解析为 mainScript 和 subPath
        /**
         * 例子:
         * dataMain = 'a', 解析出 mainScirpt = 'a', subPath = './'
         * dataMain = 'a/b', 解析出 mainScript = 'b', subpath = 'a/'
         */
        src = mainScript.split('/');
        mainScript = src.pop();
        subPath = src.length ? src.join('/')  + '/' : './';

        // 2. 设置 cfg 的 baseUrl
        cfg.baseUrl = subPath;
    }


    // 第二步:mainScript 去掉结尾的 .js
    mainScript = mainScript.replace(jsSuffixRegExp, '');

    // 第三步:如果 mainScript 仍然是路径,则回退到 dataMain
    if (req.jsExtRegExp.test(mainScript)) {
        mainScript = dataMain;
    }

    // 第四步:将 data-main 脚本放入 cfg.deps 中,等待后续加载
    cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
    
    return true;
}

经过解析,得到配置对象 cfg。比如对于 data-main="./a/b.js",得到的 cfg 如下:

cfg = {
    baseUrl: './a/',
    deps: ['b'],
}

req 加载入口 js

配置好入口文件后,下一步就是调用 req 函数,加载入口 js。第一步操作,创建默认上下文也是调用 req 函数,我们放在一起看。

defContextName = '_'

req = requirejs = function (deps, callback, errback, optional) {

    // 第一步:contextName 设置为默认值
    var context, config,
        contextName = defContextName;

    // 第二步:确定第一个参数,是否为 config 对象。如果是,重新修改其他参数。
    if (!isArray(deps) && typeof deps !== 'string') {
        // deps is a config object
        config = deps;
        if (isArray(callback)) {
            // Adjust args if there are dependencies
            deps = callback;
            callback = errback;
            errback = optional;
        } else {
            deps = [];
        }
    }

    // 第三步:根据 config 对象,更新 contextName
    if (config && config.context) {
        contextName = config.context;
    }

    // 第四步:根据 contextName 获取或新建上下文
    context = getOwn(contexts, contextName);
    if (!context) {
        context = contexts[contextName] = req.s.newContext(contextName);
    }

    // 第五步:如果有 config 对象,就更新上下文的配置
    if (config) {
        context.configure(config);
    }

    // 第六步:调用上下文的 require 方法
    return context.require(deps, callback, errback);
};

s = req.s = {
    contexts: contexts,
    newContext: newContext
};

可以看到 req 函数就是 requirejs 函数。它有两种调用方式,一种是不传入 config 对象,一种是传入。

  • 不传入 config 对象:
    • 直接使用默认上下文(获取或新建默认上下文),调用默认上下文的 require 方法。
  • 传入 config 对象:
    • 根据 config 更新 contextName,根据 contextName 获取或新建上下文(注意:如果它没有更新,也还是默认上下文)。
    • 用 config 更新上下文的配置,再调用上下文的 require 方法。

req 执行过程

req({}),传了一个没有属性的对象进去,创建了默认上下文。
req(cfg),还是以前面 data-main="./a/b.js" 为例,传入参数为 { baseUrl: './a/', deps: ['b'] },加载了入口文件。
req 前三步的执行逻辑如下:

req = requirejs = function (deps, callback, errback, optional) {

    // 第一步:contextName = '_'
    var context, config,
        contextName = defContextName;

    // 第二步:
    // req({}),第一个参数是 {},结果: config = {}, deps = []。
    // req(cfg),第一个参数是 { baseUrl: './a/', deps: ['b'] },结果:config = { baseUrl: './a/', deps: ['b'] }, deps = []
    if (!isArray(deps) && typeof deps !== 'string') {
        // config = 第一个参数
        config = deps;
        // 未传入第二个参数,deps = []
        if (isArray(callback)) {
            deps = callback;
            callback = errback;
            errback = optional;
        } else {
            deps = [];
        }
    }

    // 第三步:
    // req({}),config 为 {},所以 contextName 依然为默认值 '_'
    // req(cfg),config 为 { baseUrl: './a/', deps: ['b'] }, 所以 contextName 依然为默认值 '_'
    if (config && config.context) {
        contextName = config.context;
    }

    ...
};

我们继续看 req 函数的第四步。
req({}),一开始还没有默认上下文,所以会新建默认上下文。
req(cfg) 的 contextName 也是默认值 '_',而默认上下文已经新建,所以会直接获取默认上下文。

    // req 函数的第四步:
    // req({}),contextName 为默认值 '_',一开始没有默认上下文,所以走 req-4.2,新建默认上下文。
    // req(cfg),contextName 为默认值 '_',已经有默认上下文,所以走 req-4.1,获取默认上下文。
    req = requirejs = function (deps, callback, errback, optional) {
        ...
        // req-4.1. 根据 contextName 获取 context。
        context = getOwn(contexts, contextName);
        // req-4.2. 如果没有 context,根据 contextName 新建一个上下文。
        if (!context) {
            context = contexts[contextName] = req.s.newContext(contextName);
        }
        ...
    }

    /* req-4.1 获取上下文 */
    // getOwn 的第一个参数 contexts,初始化时是 {}
    contexts = {},

    // getOwn:判断对象中,是否有某个属性,如果有就返回属性值
    function getOwn(obj, prop) {
        return hasProp(obj, prop) && obj[prop];
    }
    
    // hasProp:判断对象中,是否有某个属性
    function hasProp(obj, prop) {
        return hasOwn.call(obj, prop);
    }

    // hasOwn
    op = Object.prototype,
    hasOwn = op.hasOwnProperty,

继续看 req-4.2 新建上下文的逻辑,由于代码量大,这里进行省略简化。

    /* req-4.2 新建上下文 */
    function newContext(contextName) {
        // 定义一堆变量和函数,这里只看 context
        var context
        ...

        // req-4.2.1:context 赋值
        context = {
            config: config,
            contextName: contextName,
            ...
            // 为上下文设置配置
            configure: function (cfg) {
                // 确保 baseUrl 以 / 结束
                if (cfg.baseUrl) {
                    if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
                        cfg.baseUrl += '/';
                    }
                }
                ...
                // configure 的最后一步:如果指定了 deps 或 callback,则使用这些参数调用 require。
                if (cfg.deps || cfg.callback) {
                    context.require(cfg.deps || [], cfg.callback);
                }
            },
            // 加载模块
            makeRequire: function (relMap, options) { ... },
            ...
        }

        // req-4.2.2:设置 context.require,并返回 context
        context.require = context.makeRequire();
        return context;
    }

可以看到新建上下文,主要是对 context 进行赋值,定义一系列的属性和方法,并返回 context。

回到 req 函数,第五步是设置 config,如果指定了 deps,则调用 require 加载模块。

第六步是返回 context.require,加载模块。

req = requirejs = function (deps, callback, errback, optional) {
    ...

    // 第五步:调用 context.configure 更新配置
    /**
     * req({}),config 为 {},结果:context.config = {config: baseUrl: "./", bundles: {}, config: {}, paths: {}, pkgs: {}, shim: {}, waitSeconds: 7}
     * req(cfg),config 为 { baseUrl: './a/', deps: ['b'] },结果:context.config = {config: baseUrl: "./a/", deps: ["b"], bundles: {}, config: {}, paths: {}, pkgs: {}, shim: {}, waitSeconds: 7}, 由于 cfg.deps 为 ['b'],在 configure 的最后一步会调用 context.require(cfg.deps),加载入口 js
     */
    if (config) {
        context.configure(config);
    }

    // 第六步:context.require,从第二步可知,deps 均为 [],所以这里没有依赖模块加载。
    return context.require(deps, callback, errback);
};

加载模块

梳理完 req 函数的执行过程,可以看到,req(cfg) 在第五步会通过 context.require(cfg.deps),加载入口 js,其中 cfg.deps 为 ['b']。

接下来,就继续查看加载模块的实现逻辑。在前面(req-4.2.2),可以看到 context.require = context.makeRequire()

context = {
    ...
    makeRequire: function (relMap, options) {
        options = options || {};

        // 第一步:定义 localRequire(require 的实现)
        function localRequire(deps, callback, errback) {
            var id, map, requireMod;

            if (options.enableBuildCallback && callback && isFunction(callback)) {
                callback.__requireJsBuild = true;
            }

            // require-1. 如果 deps 是字符串类型,根据模块名称获取模块id ,再返回 defined[id]
            if (typeof deps === 'string') {
                ...
                return defined[id];
            }

            // require-2. 抓取全局队列中等待的 defines
            intakeDefines();

            // require-3. 在 nextTick 中,加载所有依赖
            context.nextTick(function () {
                intakeDefines();

                // 获取模块加载器(重点)
                requireMod = getModule(makeModuleMap(null, relMap));

                requireMod.skipMap = options.skipMap;

                // 初始化模块(重点)
                requireMod.init(deps, callback, errback, {
                    enabled: true
                });

                checkLoaded();
            });

            return localRequire;
        }

        // 第二步:localReuire 增加 isBrowser, toUrl, defined, specified 四个方法
        mixin(localRequire, {
            isBrowser: isBrowser,
            toUrl: function (moduleNamePlusExt) { ... }, // module name + .extension 转为 url 路径
            defined: function (id) { ... },
            specified: function (id) { ... }
        });

        // 第三步:localRequire 增加 undef 方法,注意:只允许在顶级 require 调用 undef
        if (!relMap) {
            localRequire.undef = function (id) {
                ...
            };
        }

        // 第四步:返回 localRequire
        return localRequire;
    }
}

context.require = context.makeRequire()

context.makeRequire() 返回的是 localRequire,而 localRequire 使用 context.nextTick,在未来的事件循环中加载依赖。

nextTick 的实现如下:

context = {
    nextTick: req.nextTick,
};
/**
 * 在当前任务结束之后执行某些操作,具体来说是通过 setTimeout 将操作放到事件循环的消息队列中,排队等待执行。
 * 其他环境下,如果有比 setTimeout 更好的解决方案,就会改写该方法。
 * 注:延时 4ms,是因为 setTimeout 的最小延时时间为 4ms。
*/
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
    setTimeout(fn, 4);
} : function (fn) { fn(); };

继续看,nextTick 中加载模块的逻辑:

context.nextTick(function () {
    ...
    // module-1. 获取模块加载器
    requireMod = getModule(makeModuleMap(null, relMap));
    
    // module-2. 初始化模块(重点)
    requireMod.init(deps, callback, errback, {
        enabled: true
    });
    ...
});

// module-1. 获取或新建模块加载器:new context.Module(depMap)
function getModule(depMap) {
    var id = depMap.id,
        mod = getOwn(registry, id);

    if (!mod) {
        // 新建一个模块加载器
        mod = registry[id] = new context.Module(depMap);
    }

    return mod;
}

// 模块加载器:包含属性和原型方法
Module = function (map) {
    this.map = map;
    ...
};
Module.prototype = {
    ...
};
context = {
    ...
    Module: Module
}

这里通过 getModule 返回 Module 的实例,Module 包含模块相关的属性和方法。

接下来,调用 Moduleinit 方法初始化模块:

// module-2. 初始化模块
Module.prototype = {
    init: function (depMaps, factory, errback, options) {
        ...

        // 如果未启动,就启动该模块。如果已启动,检查并启动其依赖项。
        if (options.enabled || this.enabled) {
            this.enable();
        } else {
            this.check();
        }
    },
    enable: function () {
        ... 
        // module-2.1. 遍历启动依赖
        each(this.depMaps, bind(this, function (depMap, i) {
            ...
            if (!hasProp(handlers, id) && mod && !mod.enabled) {
                // 调用 context.enable 启动
                context.enable(depMap, this);
            }
        }));

        // module-2.2. 加载当前模块
        this.check();
    }
}

context = {
    ...
    enable: function (depMap) {
        // 如果模块仍在注册表中等待启动,则启动该模块。
        var mod = getOwn(registry, depMap.id);
        if (mod) {
            // 递归加载依赖项,getModule,并 enable
            getModule(depMap).enable();
        }
    },
}

module-2.1 的执行流程:this.init() -> this.enale() -> context.enable(depMap, this) -> getModule(depMap).enable() -> ...
在当前模块执行 enable() 时,遍历启动依赖模块;依赖模块执行 enable() 时,又会遍历启动它里面的依赖。就这样,通过递归,启动所有依赖模块。

继续看 module-2.2 this.check() 加载当前模块。

Module.prototype = {
    check: function () {
        ...

        if (!this.inited) {
            // 调用 this.fetch
            if (!hasProp(context.defQueueMap, id)) {
                this.fetch();
            }
        }
        ...
    },
    fetch: function () {
        ...
        // 调用 this.load。this.callPlugin 是加载插件的,最终也会调用 this.load()。
        return map.prefix ? this.callPlugin() : this.load();
    },
    load: function () {
        var url = this.map.url;

        if (!urlFetched[url]) {
            urlFetched[url] = true;
            // 调用 context.load
            context.load(this.map.id, url);
        }
    }
}

context = {
    ...
    // 执行 req.load
    load: function (id, url) {
        req.load(context, id, url);
    }
}

req.load = function (context, moduleName, url) {
    var config = (context && context.config) || {},
        node;
    if (isBrowser) {
        // 浏览器环境,新建 script 标签
        node = req.createNode(config, moduleName, url);

        // 记录上下文名和模块名
        node.setAttribute('data-requirecontext', context.contextName);
        node.setAttribute('data-requiremodule', moduleName);

        // 监听事件
        if (node.attachEvent &&
                !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                !isOpera) {
            // 兼容 IE
            useInteractive = true;

            node.attachEvent('onreadystatechange', context.onScriptLoad);
        } else {
            node.addEventListener('load', context.onScriptLoad, false);
            node.addEventListener('error', context.onScriptError, false);
        }
        // 设置 src
        node.src = url;

        if (config.onNodeCreated) {
            config.onNodeCreated(node, config, moduleName, url);
        }

        // 插入 scirpt 标签
        currentlyAddingScript = node;
        if (baseElement) {
            head.insertBefore(node, baseElement);
        } else {
            head.appendChild(node);
        }
        currentlyAddingScript = null;

        return node;
    } else if (isWebWorker) {
        ...
    }
};

// 新建 script 节点
req.createNode = function (config, moduleName, url) {
    var node = config.xhtml ?
            document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
            document.createElement('script');
    node.type = config.scriptType || 'text/javascript';
    node.charset = 'utf-8';
    node.async = true;
    return node;
};

module-2.2 加载模块的执行流程:this.check() -> this.fetch() -> this.load() -> context.load() -> req.load() -> req.createNode()
可以看到,在浏览器环境,是通过插入 <script> 标签,设置 node.src = url 来加载模块的。

define 定义模块

在 requirejs 的使用示例中,有一个 test 模块。在调用 require 函数时,会通过 context.require(deps, callback, errback),加载 test.js。

// a/b.js

// 加载模块
requirejs(['test'], function(test) {
    test.compare(2, 5)
});

而 test.js 中,通过 define 函数定义了 test 模块。

// test.js

define('test', function() {
    return {
        compare: function(a, b) {
            return a > b;
        }
    }
});

下面看一下 define 函数的实现:

commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,

define = function (name, deps, callback) {
    var node, context;

    // 第一步:没有传入 name,调整参数
    // 示例中的 test 模块,name 为 'test'
    if (typeof name !== 'string') {
        callback = deps;
        deps = name;
        name = null;
    }

    // 第二步:没有传入 deps,调整参数
    // test 模块,没有传入 deps,callback = fn,deps = null
    if (!isArray(deps)) {
        callback = deps;
        deps = null;
    }

    // 第三步:没有传入 deps, callback 为函数,从 callback 中获取依赖
    // test 模块,执行这部分代码,由于 callback 没有形式参数,结果:deps = []
    if (!deps && isFunction(callback)) {
        deps = [];
        if (callback.length) {
            callback
                .toString()
                .replace(commentRegExp, commentReplace) // 删除注释
                .replace(cjsRequireRegExp, function (match, dep) { // 获取依赖模块
                    deps.push(dep);
                });

            // Function.length 对应函数形参的个数,只有一个参数时,deps = ['require'];否则 deps = ['require', 'exports', 'module', ...deps]
            deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
        }
    }

    // 第四步:兼容 IE 6-8
    // test 模块,非 IE,不执行这部分代码
    if (useInteractive) {
        node = currentlyAddingScript || getInteractiveScript();
        if (node) {
            if (!name) {
                name = node.getAttribute('data-requiremodule');
            }
            context = contexts[node.getAttribute('data-requirecontext')];
        }
    }

    /**
     * 第五步:如果有上下文,则将依赖加入 context.defQueue。如果没有上下文,则将依赖加入全局队列。
     * 脚本 onload 时,再调用 def。这允许一个文件有多个模块,而不会过早地跟踪依赖,并支持匿名模块,其中模块名称在脚本 onload 事件发生之前是未知的。
    */
    // test 模块,由于没有执行第四步,没有获取 context,所以加入 globalDefQueue。
    if (context) {
        context.defQueue.push([name, deps, callback]);
        context.defQueueMap[name] = true;
    } else {
        globalDefQueue.push([name, deps, callback]);
    }
};

通过 define('test', function() {...}),依赖模块加入 context.defQueueglobalDefQueue 中,在脚本 onload 之后加载依赖模块。

回顾前面加载模块的流程,module-2.2 this.check() -> this.fetch() -> this.load() -> context.load() -> req.load()。在 req.load 中,会监听 test 脚本的 load 事件,并在这里再次初始化 test 模块。具体如下:

req.load = function (context, moduleName, url) {
    ...
    // 第一步:监听 script 的 load 事件,调用 context.onScriptLoad
    node.addEventListener('load', context.onScriptLoad, false);
    ...
};

function newContext(contextName) {
    var defQueue = []

    context = {
        ...
        defQueue: defQueue,
        onScriptLoad: function (evt) {
            if (evt.type === 'load' ||
                    (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
                interactiveScript = null;

                // 第二步:获取模块名,调用 completeLoad
                var data = getScriptData(evt);
                context.completeLoad(data.id);
            }
        },
        completeLoad: function (moduleName) {
            var found, args, mod,
                shim = getOwn(config.shim, moduleName) || {},
                shExports = shim.exports;

            // 第三步:获取全局依赖模块
            takeGlobalQueue();

            while (defQueue.length) {
                args = defQueue.shift();
                if (args[0] === null) {
                    args[0] = moduleName;
                    if (found) {
                        break;
                    }
                    found = true;
                } else if (args[0] === moduleName) {
                    found = true;
                }

                // 第四步:获取模块并初始化
                callGetModule(args);
            }
            context.defQueueMap = {};

            ...
        },
    }

    function takeGlobalQueue() {
        // 将 globalDefQueue 中的依赖放入 context 的 defQueue
        if (globalDefQueue.length) {
            each(globalDefQueue, function(queueItem) {
                var id = queueItem[0];
                if (typeof id === 'string') {
                    context.defQueueMap[id] = true;
                }
                defQueue.push(queueItem);
            });
            globalDefQueue = [];
        }
    }

    function callGetModule(args) {
        // 跳过已定义的模块
        if (!hasProp(defined, args[0])) {
            // 获取模块,初始化
            getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
        }
    }
}

可以看到,依然是通过 getModule 获取模块,再通过 init 初始化。初始化流程和前面 req 函数一样:this.init() -> this.enale() -> 遍历启动依赖模块(test 模块没有依赖,跳过) -> this.check(),不再赘述。不同的是,this.check() 中的流程(执行 test 模块的工厂函数,并赋值给 this.exportsdefined['test']):

Module.prototype = {
    check: function () {
        ...
        // 已经初始化过,这次不走 this.fetch()
        if (!this.inited) {
            if (!hasProp(context.defQueueMap, id)) {
                this.fetch();
            }
        } else if (this.error) {
            this.emit('error', this.error);
        } else if (!this.defining) {
            // 走这个流程
            this.defining = true;

            if (this.depCount < 1 && !this.defined) {
                if (isFunction(factory)) {
                    // 第一步:调用 context.execCb,执行 factory 函数,执行结果赋值给 exports
                    // 对于 test 模块,入参 id = "test", factory = test 模块的工厂函数, depExports = [], exports = undefined, 结果:exports = { compare: fn }
                    if ((this.events.error && this.map.isDefine) ||
                        req.onError !== defaultOnError) {
                        try {
                            exports = context.execCb(id, factory, depExports, exports);
                        } catch (e) {
                            err = e;
                        }
                    } else {
                        exports = context.execCb(id, factory, depExports, exports);
                    }
                    ...
                } else {
                    exports = factory;
                }

                this.exports = exports;

                if (this.map.isDefine && !this.ignore) {
                    // 第二步:设置 defined[id]
                    // 对于 test 模块,id = 'test', exports = { compare: fn },结果: defined['test'] = { compare: fn }
                    defined[id] = exports;
                    
                    if (req.onResourceLoad) {
                        var resLoadMaps = [];
                        each(this.depMaps, function (depMap) {
                            resLoadMaps.push(depMap.normalizedMap || depMap);
                        });
                        req.onResourceLoad(context, this.map, resLoadMaps);
                    }
                }

                cleanRegistry(id);

                this.defined = true;
            }
            
            this.defining = false;

            if (this.defined && !this.defineEmitted) {
                this.defineEmitted = true;
                // 第三步:触发 defined 事件
                this.emit('defined', this.exports);
                this.defineEmitComplete = true;
            }

        }
    },
    // 注意:这里 enable 的模块对应 require(['test'], f(test)),在该模块 enable 时遍历其依赖 ['test'],并监听依赖的 defined 事件
    enable: function () {
        ...
        each(this.depMaps, bind(this, function (depMap, i) {
            var id, mod, handler;

            if (typeof depMap === 'string') {
                ...
                this.depCount += 1;

                // 第四步:监听 defined 事件
                // test 模块,depExports = { compare: fn }
                on(depMap, 'defined', bind(this, function (depExports) {
                    if (this.undefed) {
                        return;
                    }
                    // 第五步:将 exports 存放到 this.depExports 数组中
                    this.defineDep(i, depExports);
                    // 第六步:再次执行 this.check()
                    this.check();
                }));
                ...
            }
            ...
        }
        ...
    },
    defineDep: function (i, depExports) {
        if (!this.depMatched[i]) {
            this.depMatched[i] = true;
            this.depCount -= 1;
            // 将 exports 存放到 this.depExports 数组中
            this.depExports[i] = depExports;
        }
    },
}

function newContext(contextName) {
    context = {
        execCb: function (name, callback, args, exports) {
            return callback.apply(exports, args);
        },
        ...
    }
}

这里,执行 define('test', function() {...}) 中工厂函数,得到 test 模块的对象 { compare: fn }。再将模块对象赋值给 this.exports,用于输出模块对象。同时保存到上下文的 defined 对象中,缓存起来。(第一步 - 第二步)

接着,触发 defined 事件,将 exports 存放到 this.depExports 数组中,并执行 require(['test'], f(test)) 模块的 this.check()。(第三步 - 第六步)

继续看这一次的 this.check() 流程:

Module.prototype = {
    check: function () {
        ...
        // factory = require 中的函数 f(test), depExports = [compare], exports = undefined
        // 调用 context.execCb,执行 require(['test'], function (test) {}) 中的函数,并将 depExports(即 test 模块的返回值,compare 函数) 作为参数传入。
        exports = context.execCb(id, factory, depExports, exports);
        ...
        }
    }
}

这次调用 check,执行的是 require 中传入的函数,并将 this.depExports 作为参数传入,也就是前面 test 模块的返回值 { compare: fn }。至此,test 模块加载完成。