解密javascript模块加载器require.js

require.config

require.config设置require.js模板加载选项

    // 定义config
    req.config = function (config) {
        return req(config);
    };
// 加载config配置项
req = requirejs = function (deps, callback, errback, optional) {
        var context, config,
            contextName = defContextName;

        // 如果deps是config对象
        if (!isArray(deps) && typeof deps !== ‘string‘) {
            config = deps;
            if (isArray(callback)) {
                deps = callback;
                callback = errback;
                errback = optional;
            } else {
                deps = [];
            }
        }

        if (config && config.context) {
            contextName = config.context;
        }
        // 获取默认的context
        context = getOwn(contexts, contextName);
        if (!context) {
            context = contexts[contextName] = req.s.newContext(contextName);
        }
        // 配置模块加载选项
        if (config) {
            context.configure(config);
        }
        // 当deps为config时,此调用木有实质作用
        return context.require(deps, callback, errback);
    };
// newContext中定义configure
configure: function (cfg) {
                // 确保baseUrl以/结尾
                if (cfg.baseUrl) {
                    if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== ‘/‘) {
                        cfg.baseUrl += ‘/‘;
                    }
                }

                var shim = config.shim,
                    objs = {
                        paths: true,
                        bundles: true,
                        config: true,
                        map: true
                    };

                // 将cfg中的配置信息合并到默认的context的config中
                eachProp(cfg, function (value, prop) {
                    if (objs[prop]) {
                        if (!config[prop]) {
                            config[prop] = {};
                        }
                        mixin(config[prop], value, true, true);
                    } else {
                        config[prop] = value;
                    }
                });

                //Reverse map the bundles
                if (cfg.bundles) {
                    eachProp(cfg.bundles, function (value, prop) {
                        each(value, function (v) {
                            if (v !== prop) {
                                bundlesMap[v] = prop;
                            }
                        });
                    });
                }

                // 对shim进行相应处理保存于默认context的config中
                if (cfg.shim) {
                    eachProp(cfg.shim, function (value, id) {
                        // 规范化shim结构
                        // e.g. jquery-datepicker: [‘jquery‘] -> jquery-datepicker: {deps: [‘jquery‘]}
                        if (isArray(value)) {
                            value = {
                                deps: value
                            };
                        }
                        // value.exports: 非requirejs定义文件的加载modulename,用于判断是否加载成功
                        // value.init: 是否存在初始化工作
                        if ((value.exports || value.init) && !value.exportsFn) {
                            value.exportsFn = context.makeShimExports(value);
                        }
                        shim[id] = value;
                    });
                    config.shim = shim;
                }
                ...
            }

综上:require.config将模块加载选项进行相应处理后,保存于默认的context.config中

require()

require()加载模块文件,
e.g.

require([‘jquery‘], function() {
  ...
});
req = requirejs = function (deps, callback, errback, optional) {

        var context, config,
            contextName = defContextName;

        ...

        // 获取默认的context
        context = getOwn(contexts, contextName);
        if (!context) {
            context = contexts[contextName] = req.s.newContext(contextName);
        }

        ...
        // 通过环境变量加载模块,newContext倒数几行代码中写到context.require = context.makeRequire()
        return context.require(deps, callback, errback);
    };
makeRequire: function (relMap, options) {
                options = options || {};

                function localRequire(deps, callback, errback) {
                    var id, map, requireMod;

                    ...

                    context.nextTick(function () {
                        // 将之前已define()定义的模块加载进来
                        intakeDefines();
                        // 创建require中默认的Module
                        requireMod = getModule(makeModuleMap(null, relMap));

                        ...

                        // 初始化require信息,加载deps依赖项 
                        requireMod.init(deps, callback, errback, {
                            enabled: true
                        });
                        // 根据config中waitSeconds来检查js加载是否超时,waitSeconds为0,无加载超时机制
                        // checkLoaded采用setTimeout,若未加载完即50ms后检查再次调用checkLoaded判断是否加载完毕
                        checkLoaded();
                    });

                    return localRequire;
                }
// Module中init的定义
init: function (depMaps, factory, errback, options) {

                ...

                if (options.enabled || this.enabled) {
                    // 将deps依赖项进行处理变为Module,进行加载
                    this.enable();
                } else {
                    this.check();
                }
            }
enable: function () {
                // 对depMaps的所有依赖项进行处理
                each(this.depMaps, bind(this, function (depMap, i) {
                    var id, mod, handler;

                    if (typeof depMap === ‘string‘) {

                        ...

                        on(depMap, ‘defined‘, bind(this, function (depExports) {
                            if (this.undefed) {
                                return;
                            }
                            // 跟进defineDep可以看到,将dep.exports保存在this.depExports中,用于加载后回调函数的参数传递,这是按顺序来的,回调函数调用详见下面check()中
                            this.defineDep(i, depExports);
                            this.check();
                        }));

                ...

                // 检查每个Module是否初始化、加载完模块
                this.check();
            }
check: function () {
                if (!this.enabled || this.enabling) {
                    return;
                }

                var err, cjsModule,
                    id = this.map.id,
                    depExports = this.depExports,
                    exports = this.exports,
                    factory = this.factory;

                if (!this.inited) {
                    // 获取模块所对应的js文件,源码跟踪fetch()发现实际是调用了context.load即req.load
                    this.fetch();
                } else if (this.error) {
                    this.emit(‘error‘, this.error);
                } else if (!this.defining) {
                    // deps依赖项已加载完毕,准备回调
                    if (this.depCount < 1 && !this.defined) {
                        if (isFunction(factory)) {

                            // 通过context.execCb调用回调函数,将刚保存的depExports的参数传入回调函数中,完成模块的依赖注入
                            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);
                            }    
            }

        ...
req.load = function (context, moduleName, url) {
        var config = (context && context.config) || {},
            node;
        // 是浏览器还是Webworker
        if (isBrowser) {
            // 通过 document.createElement(‘script‘)创建script节点,同时设置node.async = true实现异步加载
            node = req.createNode(config, moduleName, url);

            node.setAttribute(‘data-requirecontext‘, context.contextName);
            node.setAttribute(‘data-requiremodule‘, moduleName);
            // 依据的浏览器的不同添加script的load、error事件
            if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf(‘[native code‘) < 0) &&
                    !isOpera) {

                useInteractive = true;

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

            currentlyAddingScript = node;
            if (baseElement) {
                head.insertBefore(node, baseElement);
            } else {
                head.appendChild(node);
            }
            currentlyAddingScript = null;

            return node;
        } else if (isWebWorker) {
            try {
                // WebWorker采用importScripts加载js文件
                importScripts(url);

                context.completeLoad(moduleName);
            } catch (e) {
                context.onError(makeError(‘importscripts‘,
                                ‘importScripts failed for ‘ +
                                    moduleName + ‘ at ‘ + url,
                                e,
                                [moduleName]));
            }
        }
    }

综上:require()就是对需要的依赖模块进行相应的url、是否在path、或存在shim等的处理后转换为Module。加载js文件有以下两种方式:
Browser:document.createElement创建script,然后通过设置asyn=true,head.appendChild(node)
WebWorker:importScripts(url)

define()

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

        // 对实参不同情况做相应处理
        ...

        if (!deps && isFunction(callback)) {
            deps = [];
            // 通过toString()然后正则表达式将define()定义的依赖项保存在deps中
            if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp, ‘‘)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

                ...
            }
        }

        ...

        // 将define定义的信息存放在默认context.defQueue或globalDefQueue中,等定义define()的js文件加载完后,在通过来处理这些define信息
        // 上面require()中js文件加载过程分析,可知js文件加载后将会调用事件函数onScriptLoad
        (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
    }
onScriptLoad: function (evt) {
                // 对文件加载的状态进行判断
                if (evt.type === ‘load‘ ||
                        (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {

                    interactiveScript = null;

                    // 将script的添加的事件函数进行清除,同时获取node的信息
                    var data = getScriptData(evt);
                    // 通过默认context来加载该data所对应的Module
                    context.completeLoad(data.id);
                }
            }
completeLoad: function (moduleName) {
                var found, args, mod,
                    shim = getOwn(config.shim, moduleName) || {},
                    shExports = shim.exports;

                // 将globalDefQueue合并到context.defQueue中
                takeGlobalQueue();

                // 找到moduleName所对应的的Module
                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;
                    }

                    // 获取args模块
                    callGetModule(args);
                }

                ...
            }
function callGetModule(args) {
            // 若该模块没有被加载,即加载该模块,然后调用Module.init(),这个过程在require()已分析,即初始化、加载模块了
            if (!hasProp(defined, args[0])) {
                getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
            }
        }

综上:define()模块定义,仅将模块信息加载到默认context.defQueue或者globalDefQueue中,而处理这些信息是在定义define()的js文件加载完后进行的。

整体过程

  • 加载require.js库文件,生产默认的context
  • 查找data-main入口点,require.config()将模块加载选项做相应处理,然后加入默认的context中
  • 加载require()依赖项,通过document.createElement和设置asyn或者importScripts来加载js文件
  • 加载过程中发现define()模块定义,将模块定义信息加载到默认的context.defQueue或者globalDefQueue中,等定义define()的js文件加载后再处理这些模块定义信息

    其中:

  • js文件若未加载成功,采用setTimeout 50ms方式来循环判断是否加载完毕

  • define()的依赖项提取采用function.toString() + 正则表达式
  • url的转换、Module的生产等处理过程可详见源码

require.js源代码解密完毕,是不是觉得原来就这样实现的= =
其实很多事情并没有那么难,只要你去分析它就会清楚很多

小小吐槽:
require.js源代码代码规范、结构等感觉有点乱…
看过后只想说”呵 呵”

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。