lsa2127291/blog

模块加载器源码分析之requiresJs篇(二)

Opened this issue · 0 comments

前言

我在第一篇中讲解了如何使用requirejs,以及将requirejs下载下来经历的几个步骤,算是对整体结构进行了一层剖析,在这一篇章中,我会对requirejs的接下几个流程做一些分析。

requirejs的执行过程

data-main入口点

昨天提到一旦载入requirejs就会执行require(cfg),而在设置了data-main="xxx.js"之后,requireJs将会把baserUrl设置为入口文件的所在目录,然后把deps的值设为mainScript,即xxx.js的值。具体代码:

    if (isBrowser && !cfg.skipDataMain) {
        //Figure out baseUrl. Get it from the script tag with require.js in it.
        eachReverse(scripts(), function (script) {
            //Set the 'head' where we can append children by
            //using the script's parent.
            if (!head) {
                head = script.parentNode;
            }

            //Look for a data-main attribute to set main script for the page
            //to load. If it is there, the path to data main becomes the
            //baseUrl, if it is not already set.
            dataMain = script.getAttribute('data-main');
            if (dataMain) {
                //Preserve dataMain in case it is a path (i.e. contains '?')
                mainScript = dataMain;

                //Set final baseUrl if there is not already an explicit one,
                //but only do so if the data-main value is not a loader plugin
                //module ID.
                if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {
                    //Pull off the directory of data-main for use as the
                    //baseUrl.
                    src = mainScript.split('/');
                    mainScript = src.pop();
                    subPath = src.length ? src.join('/')  + '/' : './';

                    cfg.baseUrl = subPath;
                }

                //Strip off any trailing .js since mainScript is now
                //like a module name.
                mainScript = mainScript.replace(jsSuffixRegExp, '');

                //If mainScript is still a path, fall back to dataMain
                if (req.jsExtRegExp.test(mainScript)) {
                    mainScript = dataMain;
                }

                //Put the data-main script in the files to load.
                cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];

                return true;
            }
        });
    }

然后我们之前提到的context.configure(cfg),在cfg.deps不为空的时候,会执行:

                if (cfg.deps || cfg.callback) {
                    context.require(cfg.deps || [], cfg.callback);
                }

也就是说会执行两次context.require,至于为什么要这么做,目前我还是不理解,我认为当下载下来可以通过判断来确定是否执行context.require,毕竟执行一次这个函数是有一定成本的。接下来会做昨天提到的几件事,但是这次有了依赖而不是空值,会真正去执行fectch函数了,这里说明一下执行流程:凡是执行了init函数的模块,不会再去fetch,即一是require模块本身不会去fetch,二是执行了define后的模块不会去fetch,而在enable递归中得到的依赖模块会执行fetch

详解依赖加载

fetch

代码:

            fetch: function () {
                if (this.fetched) {
                    return;
                }
                this.fetched = true;

                context.startTime = (new Date()).getTime();

                var map = this.map;
                //console.log(map);
                //If the manager is for a plugin managed resource,
                //ask the plugin to load it now.
                if (this.shim) {
                    context.makeRequire(this.map, {
                        enableBuildCallback: true
                    })(this.shim.deps || [], bind(this, function () {
                        return map.prefix ? this.callPlugin() : this.load();
                    }));
                } else {
                    //Regular dependency.
                    return map.prefix ? this.callPlugin() : this.load();
                }
            },

在fetch中会执行load函数,即开始生成script元素并append到header下面,代码:

            //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);

            //Set up load listener. Test attachEvent first because IE9 has
            //a subtle issue in its addEventListener and script onload firings
            //that do not match the behavior of all other browsers with
            //addEventListener support, which fire the onload event for a
            //script right after the script execution. See:
            //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
            //UNFORTUNATELY Opera implements attachEvent but does not follow the script
            //script execution mode.
            if (node.attachEvent &&
                    //Check if node.attachEvent is artificially added by custom script or
                    //natively supported by browser
                    //read https://github.com/requirejs/requirejs/issues/187
                    //if we can NOT find [native code] then it must NOT natively supported.
                    //in IE8, node.attachEvent does not have toString()
                    //Note the test for "[native code" with no closing brace, see:
                    //https://github.com/requirejs/requirejs/issues/273
                    !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                    !isOpera) {
                //Probably IE. IE (at least 6-8) do not fire
                //script onload right after executing the script, so
                //we cannot tie the anonymous define call to a name.
                //However, IE reports the script as being in 'interactive'
                //readyState at the time of the define call.
                useInteractive = true;

                node.attachEvent('onreadystatechange', context.onScriptLoad);
                //It would be great to add an error handler here to catch
                //404s in IE9+. However, onreadystatechange will fire before
                //the error handler, so that does not help. If addEventListener
                //is used, then IE will fire error before load, but we cannot
                //use that pathway given the connect.microsoft.com issue
                //mentioned above about not doing the 'script execute,
                //then fire the script load event listener before execute
                //next script' that other browsers do.
                //Best hope: IE10 fixes the issues,
                //and then destroys all installs of IE 6-9.
                //node.attachEvent('onerror', context.onScriptError);
            } else {
                node.addEventListener('load', context.onScriptLoad, false);
                node.addEventListener('error', context.onScriptError, false);
            }
            node.src = url;

            //Calling onNodeCreated after all properties on the node have been
            //set, but before it is placed in the DOM.
            if (config.onNodeCreated) {
                config.onNodeCreated(node, config, moduleName, url);
            }

            //For some cache cases in IE 6-8, the script executes before the end
            //of the appendChild execution, so to tie an anonymous define
            //call to the module name (which is stored on the node), hold on
            //to a reference to this node, but clear after the DOM insertion.
            currentlyAddingScript = node;
            if (baseElement) {
                head.insertBefore(node, baseElement);
            } else {
                head.appendChild(node);
            }
            currentlyAddingScript = null;

            return node;

注意到两件事,一是node中加上了asnyc属性,意味着js文件将异步加载和异步执行(前提是ie10以上的版本 ),二是为标签加上了监听事件,当下载并执行完js文件后,在执行completeLoad函数。在这里,必须重点说明下js中所执行的函数define。
####define
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 = [];
            //Remove comments from the callback string,
            //look for require calls, and pull them into the dependencies,
            //but only if there are function args.
            if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp, commentReplace)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

                //May be a CommonJS thing even without require calls, but still
                //could use exports, and module. Avoid doing exports and module
                //work though if it just needs require.
                //REQUIRES the function to expect the CommonJS variables in the
                //order listed below.
                deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
            }
        }

        //If in IE 6-8 and hit an anonymous define() call, do the interactive
        //work.
        if (useInteractive) {
            node = currentlyAddingScript || getInteractiveScript();
            if (node) {
                if (!name) {
                    name = node.getAttribute('data-requiremodule');
                }
                context = contexts[node.getAttribute('data-requirecontext')];
            }
        }

        //Always save off evaluating the def call until the script onload handler.
        //This allows multiple modules to be in a file without prematurely
        //tracing dependencies, and allows for anonymous module support,
        //where the module name is not known until the script onload event
        //occurs. If no context, use the global queue, and get it processed
        //in the onscript load callback.
        if (context) {
            context.defQueue.push([name, deps, callback]);
            context.defQueueMap[name] = true;
        } else {
            globalDefQueue.push([name, deps, callback]);
            //console.log(globalDefQueue[0]);
        }

    };

它会起到如下几个作用:

  • 1.支持commonjs规范,它会根据参数进行判断当前模块使用哪种规范,如果是commonjs规范就用正则将require里请求的js模块全部提取出来,作为当前模块的依赖。
  • 2.把模块名,依赖和工厂函数一同加入全局或者当前上下文的队列中。

define在下载js文件后的第一时间执行,经过它的处理之后会进入到completeLoad函数。

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

在completeLoad函数中会defQueue中的模块参数导出并调用callGetModule重新生成模块,原因应该是模块可能是有名字的,然后调用init函数初始化模块的依赖和工厂函数,并继续执行enable函数将其依赖转换为模块,然后调用check去fetch或者exec模块。最后把执行完的模块加入的define中保存,避免多次执行,并通知依赖它的模块执行defineMap,把它的执行值加入模块的依赖值数组中,并把依赖项减1,代码:

            defineDep: function (i, depExports) {
                //Because of cycles, defined callback for a given
                //export can be called more than once.
                if (!this.depMatched[i]) {
                    this.depMatched[i] = true;
                    this.depCount -= 1;
                    this.depExports[i] = depExports;
                }
            }

最后再度执行check函数,就这样层层递归并层层返回,直到所有模块执行完毕,这就是整个require的执行过程。

execCb

最后还要提到execCb这个函数,它是执行模块的函数,代码:

                            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);
                            }
            execCb: function (name, callback, args, exports) {
                return callback.apply(exports, args);
            }

在这里args为模块的depExports,callback是工厂函数,执行环境为exports,最后将其执行结果返回给exports。

commonJS

requireJs不仅支持amd规范,还同时支持commonJs规范,下面介绍下它是如何来支持commonJs的。
首先在前面提到过define的时候会将commonJs工厂函数中require过的所有模块都加入到deps中,然后在handlers中定义了工厂函数中的require,exports,module三个函数的实现。代码:

        handlers = {
            'require': function (mod) {
                if (mod.require) {
                    return mod.require;
                } else {
                    return (mod.require = context.makeRequire(mod.map));
                }
            },
            'exports': function (mod) {
                mod.usingExports = true;
                if (mod.map.isDefine) {
                    if (mod.exports) {
                        return (defined[mod.map.id] = mod.exports);
                    } else {
                        return (mod.exports = defined[mod.map.id] = {});
                    }
                }
            },
            'module': function (mod) {
                if (mod.module) {
                    return mod.module;
                } else {
                    return (mod.module = {
                        id: mod.map.id,
                        uri: mod.map.url,
                        config: function () {
                            return getOwn(config.config, mod.map.id) || {};
                        },
                        exports: mod.exports || (mod.exports = {})
                    });
                }
            }
        }

从之前的分析中我们知道factory函数真正执行是在所有的依赖模块都已经defined之后,所以require的作用就是从definded的队列中直接取出模块,exports则是把其值加入defined队列,以暴露它自己,module则封装了模块id,exports等一系列模块,可以看作是mod的缩影,而mod就是requrire的模块本身。通过以上的方法,最终获得了对commonJs的支持。

结语

对于requireJs的分析到此告一段落,通过几天不断摸索,基本摸清了整个requireJs的工作流程,但其细节以及扩展功能和错误捕获等等,还无法完全理解,不得不承认自己离大师的境界还差的太远,同时还感到很多问题如果不是在实际中真正遇到,你是无法真正理解为什么它的代码会这么去写。所以在分析别人源码的时候,也不能过于深究,重点在摸清整体结构,从中掌握一些优秀技巧和设计模式,最终还是要自己去真正动手开发。