requirejs 源码解析

javascript Oct 12, 2021

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 模块加载完成。

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.