Require.js 源码分析

本文将简单介绍下个人对require.js的源码分析,简单分析实现原理

 

一.require加载资源的流程

require中,根据AMD(Asynchronous Module Definition)的思想,即异步模块加载机制,其思想就是把代码分为一个个的模块来分块按需加载,这样我们可以组装很多UI或者功能组件,从而实现代码的复用性。

 

1.data-main 函数从requirejs方法开始调用,newContext方法调用makeRequire方法。

1.1 makeRequire方法判断module是否defined,如果是yes就调用localRequire方法  其中可以通过getModule获取已保存的资源

1.2 如果module没有defined,调用makeRequire方法通过 递归 迭代 不停的注册module 然后放入makeModuleMap中,调用callGetModule方法

1.3 callGetModule方法只用来加载js资源的 module.init 调用 module.fetch 调用 module.load 最后调用 req.load   req.load 在遍历 调用req.createNode

1.4 其中有个checkLoaded方法使用定时器不停扫描资源加载状态,这里有3种状态

1.4.1 stillLoading 还在加载     enabling 对该模块的依赖进行加载和模块化   defining 对正在处理的模块进行加载,并运行模块中的callback  enabled处理完成的

 

如果判断js加载完成,调用原生js的一些监听事件  如:onreadystatechange 

 req.load = function (context, moduleName, url) {
        var config = (context && context.config) || {},
            node;
        if (isBrowser) {
            //In the browser so use a script tag
            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) {

                useInteractive = true;

                node.attachEvent('onreadystatechange', context.onScriptLoad);

            } else {
                node.addEventListener('load', context.onScriptLoad, false);
                node.addEventListener('error', context.onScriptError, false);
            }
            node.src = url;
            if (config.onNodeCreated) {
                config.onNodeCreated(node, config, moduleName, url);
            }

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

            return node;
...

 

如何引入资源  ,其实是插入标签,async等于true,

 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;
    };

 

但事实是我们在查看源代码的时候没有看到很多<script>标签,因为require.js最后在加载完成后删除了,在checkLoaded方法种会扫描资源加载状态,最后把加载完毕的资源删除。

function removeScript(name) {
            if (isBrowser) {
                each(scripts(), function (scriptNode) {
                    if (scriptNode.getAttribute('data-requiremodule') === name &&
                            scriptNode.getAttribute('data-requirecontext') === context.contextName) {
                        scriptNode.parentNode.removeChild(scriptNode);
                        return true;
                    }
                });
            }
        }

... 

if (!mod.inited && expired) { if (hasPathFallback(modId)) { usingPathFallback = true; stillLoading = true; } else { noLoads.push(modId); removeScript(modId); } }
...

 

二.require定义模块,这个函数根据参数类型和个数,最后定义module的依赖和回调,还记得官网大篇幅的文档如果定义module吧

http://requirejs.org/docs/api.html#define,一一揣摩就明白参数和回调的堆栈。

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

        //Allow for anonymous modules
        if (typeof name !== 'string') {
            //Adjust args appropriately
            callback = deps;
            deps = name;
            name = null;
        }

        //This module may not have dependencies
        if (!isArray(deps)) {
            callback = deps;
            deps = null;
        }

        //If no name, and callback is a function, then figure out if it a
        //CommonJS thing with dependencies.
        if (!deps && isFunction(callback)) {
            deps = [];

            if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp, commentReplace)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

                deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
            }
        }

        if (useInteractive) {
            node = currentlyAddingScript || getInteractiveScript();
            if (node) {
                if (!name) {
                    name = node.getAttribute('data-requiremodule');
                }
                context = contexts[node.getAttribute('data-requirecontext')];
            }
        }

        if (context) {
            context.defQueue.push([name, deps, callback]);
            context.defQueueMap[name] = true;
        } else {
            globalDefQueue.push([name, deps, callback]);
        }
    };

    define.amd = {
        jQuery: true
    };

在这里用到了2个很长的正则

commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,  替换回调种的注释
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, 匹配require中的参数,把产生提取出来,后面在判断是否Common.js来拼接Commonjs的三元素

最后重新附加到参数队尾。

 

三.require方法内部的主要功能实现

3.1 加载依赖资源什么时候才结束呢,这里require会在load方法中 触发js加载事件 回调onScriptLoad

onScriptLoad方法会调用completeLoad方法,

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 already found an anonymous module and bound it
                        //to this name, then this is some other anon module
                        //waiting for its completeLoad to fire.
                        if (found) {
                            break;
                        }
                        found = true;
                    } else if (args[0] === moduleName) {
                        //Found matching define call for this script!
                        found = true;
                    }

                    callGetModule(args);
                }
                context.defQueueMap = {};

                //Do this after the cycle of callGetModule in case the result
                //of those calls/init calls changes the registry.
                mod = getOwn(registry, moduleName);

                if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
                    if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
                        if (hasPathFallback(moduleName)) {
                            return;
                        } else {
                            return onError(makeError('nodefine',
                                             'No define call for ' + moduleName,
                                             null,
                                             [moduleName]));
                        }
                    } else {
                        //A script that does not call define(), so just simulate
                        //the call for it.
                        callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
                    }
                }

                checkLoaded();
            },

 

这个函数主要 遍历defQueue,获取一个module,传递moduleName,并调用callGetModule去调用模块

   function callGetModule(args) {
            //Skip modules already defined.
            if (!hasProp(defined, args[0])) {
                getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
            }
        }

callGetModule会调用函数判断是否有这个moduleName的module是否定义了,如果没定义使用makeModuleMap创建一个module返回

在makeModuleMap可以看到一个module有那些属性,为了保证moduleId唯一性,可以看到有前缀后缀,以及id拼接规则。

suffix = prefix && !pluginModule && !isNormalized ?
                     '_unnormalized' + (unnormalizedCounter += 1) :
                     '';

            return {
                prefix: prefix,
                name: normalizedName,
                parentMap: parentModuleMap,
                unnormalized: !!suffix,
                url: url,
                originalName: originalName,
                isDefine: isDefine,
                id: (prefix ?
                        prefix + '!' + normalizedName :
                        normalizedName) + suffix
            };

 

getModule方法有是如何实现的,在require内部定义了Module,而这个方法则会为当前的ModuleMap中,其中包含了这个模块的路径等信息。这里要注意的是getModule方法里面拥有一个基于registry变量中,这里则是用缓存根据ModuleMap来实例化的Module,并将其保存在了registry变量中。

这里我们也可以看到Module类的定义

Module = function (map) {
    this.events = getOwn(undefEvents, map.id) || {};
    this.map = map;
    this.shim = getOwn(config.shim, map.id);
    this.depExports = [];
    this.depMaps = [];
    this.depMatched = [];
    this.pluginMaps = {};
    this.depCount = 0;
};

Module.prototype = {
    //init Module
    init : function (depMaps, factory, errback, options) {},

    //define dependencies
    defineDep : function (i, depExports) {},

    //call require for plugins
    fetch : function () {},

    //use script to load js
    load : function () {},

    //Checks if the module is ready to define itself, and if so, define it.
    check : function () {},

    //call Plugins if them exist and defines them
    callPlugin : function () {},

    //enable dependencies and call defineDep
    enable : function () {},

    //register event
    on : function (name, cb) {},

    //trigger event
    emit : function (name, evt) {}
}

创建一个Module的时候,会使用init方法,其中enable和check方法是module重要的方法 ,enable遍历调用check方法,全部依赖都检查无误之后,改变(资源加载中)enabling状态为false,

check方法不停的遍历依赖,最后改变module状态defined为真。

 

3.2多层依赖如何加载,重复加载的问题如何解决,比如 A依赖 BC  ,B依赖 CD  C依赖 DE

当如果我去requrie(A)时,require去查找defined中是否有A模块,如果没有,则去调用makeModuleMap来为即将调用的模块实例一个ModuleMap并加入到defined中,再用ModuleMap实例化一个Module加入到registry中,但是这时候的Module是一个空壳,它是只存储了一些模块相关的依赖等,模块里的exports或者callback是还没有被嵌进来,因为这个文件根本没有被加载

只有在触发module.init方法的时候才会真正的加载资源文件,但是如何保证加载是否重复,这时候会用到chekLoaded()方法,这个方法会检查依赖是否已经define的

        function checkLoaded() {
            var err, usingPathFallback,
                waitInterval = config.waitSeconds * 1000,
                //It is possible to disable the wait interval by using waitSeconds of 0.
                expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
                noLoads = [],
                reqCalls = [],
                stillLoading = false,
                needCycleCheck = true;

            //Do not bother if this call was a result of a cycle break.
            if (inCheckLoaded) {
                return;
            }

            inCheckLoaded = true;

            //Figure out the state of all the modules.
            eachProp(enabledRegistry, function (mod) {
                var map = mod.map,
                    modId = map.id;

                //Skip things that are not enabled or in error state.
                if (!mod.enabled) {
                    return;
                }

                if (!map.isDefine) {
                    reqCalls.push(mod);
                }

                if (!mod.error) {
                    //If the module should be executed, and it has not
                    //been inited and time is up, remember it.
                    if (!mod.inited && expired) {
                        if (hasPathFallback(modId)) {
                            usingPathFallback = true;
                            stillLoading = true;
                        } else {
                            noLoads.push(modId);
                            removeScript(modId);
                        }
                    } else if (!mod.inited && mod.fetched && map.isDefine) {
                        stillLoading = true;
                        if (!map.prefix) {
                            //No reason to keep looking for unfinished
                            //loading. If the only stillLoading is a
                            //plugin resource though, keep going,
                            //because it may be that a plugin resource
                            //is waiting on a non-plugin cycle.
                            return (needCycleCheck = false);
                        }
                    }
                }
            });

            if (expired && noLoads.length) {
                //If wait time expired, throw error of unloaded modules.
                err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
                err.contextName = context.contextName;
                return onError(err);
            }

            //Not expired, check for a cycle.
            if (needCycleCheck) {
                each(reqCalls, function (mod) {
                    breakCycle(mod, {}, {});
                });
            }

            //If still waiting on loads, and the waiting load is something
            //other than a plugin resource, or there are still outstanding
            //scripts, then just try back later.
            if ((!expired || usingPathFallback) && stillLoading) {
                //Something is still waiting to load. Wait for it, but only
                //if a timeout is not already in effect.
                if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
                    checkLoadedTimeoutId = setTimeout(function () {
                        checkLoadedTimeoutId = 0;
                        checkLoaded();
                    }, 50);
                }
            }

            inCheckLoaded = false;
        }

 

定义依赖方法,官网推荐使用

define(dependencies, callback)

而不使用

define( callback(

  require(a);

  require(b);

));

 

第二种方法会先把callback.toString,然后查找require,效率上差了些。

不管是第一种还是第二种,都必须依赖加载完成的时候才能回调,所以日常工作中我们需要确定依赖的先后顺序,一些主要暂时模块需要优先加载,一些不影响主页面的东西依赖的优先级要放低,依赖要原子项,不能多方依赖,多方依赖导致仅仅一个小功能需要加载很多额外的东西

 

posted @ 2017-03-15 20:23  天下雨水  阅读(1420)  评论(0编辑  收藏  举报