RequireJs 源码解读及思考

时间:2021-06-18 16:13:22

写在前面:

最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码。现在项目结束,终于有机会好好研究一下。

本文重要解读requirejs的源码,backbone源码分析将会随后给出。

行文思路:

  • requirejs 基本介绍
  • requirejs使用后的几个好奇
  • requirejs源码解读

requirejs基本介绍

由于篇幅有限,这里不会详解requirejs的使用和api,建议读者朋友自己去用几次,再详读api.

简介

简单来说,requirejs是一个遵循 AMD(Asynchronous Module Definition)规范的模块载入框架。

使用requirejs的好处是:异步加载、模块化、依赖管理

使用

引入:

    <script data-main="/path/to/init" src="/path/to/require.js"></script>

这里的data-main是整个项目的入口.

定义模块:

   

  define( ' jquery ', function($) {
         return {
            hello: function() {
                console.log( ' hello ');
            }
        };
    });

配置:

  
  require.config({
        baseUrl:  " /client/src/script/js/ " ,
        paths: {
            jquery:  " third_party_lib/jquery "
        } ,
        shim: {
            jquery: {
                exports:  " $ "
            }
        },     
        waitSeconds: 1s,
   }); 

    

非AMD规范插件使用(很多插件和requirejs结合不能使用的解决方案)

A: AMD化. 在插件最外层包一层 define({});

B: config.shim 的exports选项. 作用是把全局变量引入.

requirejs使用后的几个好奇

  • 使用require后页面引入的js两个data-*属性,有什么用?
  •  RequireJs 源码解读及思考
  • data-main 是项目的唯一入口,但是怎么进入这个入口,之后又做了什么?
  • require和define里面怎么执行的,如何管理依赖关系?
  • require和define区别是什么?
  • 如何异步加载js文件的?

requirejs源码解读

本文的  require.js version = “2.1.11”

requirejs源码的组织:

 

如注释,整个源码我分成了三个部分。

  
    var requirejs, require, define;
    (function ( global) {
         var req, s, head, baseElement, dataMain, src,..;
        function isFunction(it) {}
        function isArray(it) {}
        function each(ary, func) {}
        function eachReverse(ary, func) {}
        function hasProp(obj, prop) {}
        function getOwn(obj, prop) {}
        function eachProp(obj, func) {}
        function mixin(target, source, force, deepStringMixin) {} ...;
         //  第一部分结束
        function newContext(contextName) {
             var inCheckLoaded, Module, context, handlers,
            function trimDots(ary) {}
            function normalize(name, baseName, applyMap) {}
            function removeScript(name) {}
            function hasPathFallback(id) {}
            function splitPrefix(name) {}
            function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {}
            function getModule(depMap) {}
            function on(depMap, name, fn) {}
            function onError(err, errback) {}
            function takeGlobalQueue() {}
            handlers = {};
            function cleanRegistry(id) {}
            function breakCycle(mod, traced, processed) {}
            function checkLoaded() {}
            Module = function (map) {};
            Module.prototype = {};
            function callGetModule(args) {}
            function removeListener(node, func, name, ieName) {}
            function getScriptData(evt) {}
            function intakeDefines() {}
            context = {}
            context.require = context.makeRequire();
             return context;
        }
         //  第二部分结束

        req = requirejs = function (deps, callback, errback, optional) {};
        req.config = function (config) {};
        req.nextTick =  typeof setTimeout !==  ' undefined ' ? function (fn) {
            setTimeout(fn,  4);
        } : function (fn) { fn(); };

        req.onError = defaultOnError;
        req.createNode = function (config, moduleName, url) {};
        req.load = function (context, moduleName, url) {};
        define = function (name, deps, callback) {};
        req.exec = function (text) {};
        req(cfg);
         //  第三部分结束
  }( this ));

     

第一部分是定义一些全局变量和helper function. 第二部分和第三部分会频繁用到。

第二部分是定义newContext的一个func, 项目的核心逻辑。

第三部分是定义require和define两个func以及项目入口。

详细分析:

data-main入口实现:

  
     //  项目入口, 寻找有data-main的script. 
     if (isBrowser && !cfg.skipDataMain) {
         //  scripts()返回页面所有的script. 逆向遍历这些script直到有一个func返回true
        eachReverse(scripts(), function (script) {
             if (!head) {
                head = script.parentNode;
            }
            dataMain = script.getAttribute( ' data-main ');
             if (dataMain) {
                mainScript = dataMain;
                 //  如果config没有配置baseUrl, 则含有data-main 指定文件所在的目录为baseUrl.
                 if (!cfg.baseUrl) {
                     //  data-main 指向'./path/to/a', 则mainScript为a.js, baseUrl 为./path/to
                    src = mainScript.split( ' / ');
                    mainScript = src.pop();
                    subPath = src.length ? src.join( ' / ')  +  ' / ' :  ' ./ ';
                    cfg.baseUrl = subPath;
                }
                 //  如果mainScript包括.js则去掉,让他表现的像一个module name
                mainScript = mainScript.replace(jsSuffixRegExp,  '');
                 if (req.jsExtRegExp.test(mainScript)) {
                    mainScript = dataMain;
                }
                 //  把data-main指向的script放入cfg.deps中, 作为第一个load
                cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
                 return  true;
            }
        });
   }  

   

define实现:

   

  define = function (name, deps, callback) {
         var node, context;
         //  匿名模块
         if ( typeof name !==  ' string ') {
             //  第一个参数调整为deps, 第二个callback
            callback = deps;
            deps = name;
            name =  null;
        }

         //  没有deps
         if (!isArray(deps)) {
            callback = deps;
            deps =  null;
        }
         if (!deps && isFunction(callback)) {
            deps = [];
             //  .toString 把callback变成字符串方便调用字符串的func. 
            
//  .replace 把所有注释去掉. commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
            
//  .replace 把callback里面的  require全部push 到 deps
             if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp,  '')
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });
                deps = (callback.length ===  1 ? [ ' require '] : [ ' require '' exports '' module ']).concat(deps);
            }
        }

         //  如果IE6-8有匿名define, 修正name和context的值
         if (useInteractive) {
            node = currentlyAddingScript || getInteractiveScript();
             if (node) {
                 if (!name) {
                    name = node.getAttribute( ' data-requiremodule ');
                }
                context = contexts[node.getAttribute( ' data-requirecontext ')];
            }
        }

         //  如果当前context存在, 把本次define加入到defQueue中
        
//  否则加入globalDefQueue
        (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);

    };

加载js文件:

     

   req.load = function (context, moduleName, url) {
         var config = (context && context.config) || {},
            node;
         if (isBrowser) {

            node = req.createNode(config, moduleName, url);

             //  所以每个通过requirej引入的js文件有这两个属性,用来移除匹配用的.或则IE低版本中修正contextName和moduleName
            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;

             //  兼容IE6-8, script可能在append之前执行, 所有把noe绑定在currentAddingScript中,防止其他地方改变这个值
            currentlyAddingScript = node;
             if (baseElement) {
                 //  这里baseElement是getElementsByName('base'); 现在一般都执行else了。
                head.insertBefore(node, baseElement);
            }  else {
                head.appendChild(node);
            }
            currentlyAddingScript =  null;
             return node;
        }  else  if (isWebWorker) {
             //  如果是web worker。。不懂
             try {
                importScripts(url);
                context.completeLoad(moduleName);
            }  catch (e) {
                context.onError(makeError( ' importscripts ',
                                 ' importScripts failed for  ' +
                                    moduleName +  '  at  ' + url,
                                e,
                                [moduleName]));
            }
        }

    };

这里可以看出第一个问题的原因了.引入data-*的作用是用来移除匹配用的.或则IE低版本中修正contextName和moduleName. 这里req.createNode和context.onScriptLoad是其他地方定义的,接下来看req.createNope:

 

   

  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;

    };

这里可以解决最后一个问题,通过appendChild, node.async实现异步加载的。

当node加载完毕后会调用context.onScriptLoad, 看看做了什么:

   

  onScriptLoad:  function (evt) {
         if (evt.type === 'load' ||
                (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
            interactiveScript =  null;
             //  getScriptData()找evet对应的script, 提取data-requiremodule就知道mod的name了。
             var data = getScriptData(evt);
            context.completeLoad(data.id);
        }

    }

再看context.completeLoad:

   

  completeLoad:  function (moduleName) {

         var found, args, mod,
            shim = getOwn(config.shim, moduleName) || {},
            shExports = shim.exports;
         //  把globalQueue 转换到 context.defQueue(define收集到的mod集合)
        takeGlobalQueue();
         while (defQueue.length) {
            args = defQueue.shift();
             if (args[0] ===  null) {
                args[0] = moduleName;
                 //  如果当前的defModule是匿名define的(arg[0]=null), 把当前moduleName给他,并标记找到
                 if (found) {
                     break;
                }
                found =  true;
            }  else  if (args[0] === moduleName) {
                 //   非匿名define
                found =  true;
            }
             //  callGetModule较长, 作用是实例化一个context.Module对象并初始化, 放入registry数组中表示可用.
            callGetModule(args);
        }

         //  获取刚才实例化的Module对象
        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 {
                callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
            }
        }
         //  检查loaded情况,超过时间的就remove掉,并加入noLoads数组
        checkLoaded();

    }

可以看到,当script加载完毕后,只做了一件事:实例化context.Module对象,并暴露给registry供调用.

require 实现

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

         var context, config,
            contextName = defContextName;
         //  第一个参数不是模块依赖表达
         if (!isArray(deps) &&  typeof deps !== 'string') {
             //  deps is a config object
            config = deps;
             if (isArray(callback)) {
                 //  如果有依赖模块则调整参数, 第二个参数是deps
                deps = callback;
                callback = errback;
                errback = optional;
            }  else {
                deps = [];
            }
        }
         if (config && config.context) {
            contextName = config.context;
        }
        context = getOwn(contexts, contextName);
         if (!context) {
            context = contexts[contextName] = req.s.newContext(contextName);
        }
         if (config) {
            context.configure(config);
        }
         //  以上获取正确的context,contextName
         return context.require(deps, callback, errback);
    }; 

 

一看,结果什么都没做,做的事还在context.require()里面。 在context对象中:

     context.require = context.makeRequire();

我们需要的require结果是context.makeRequire这个函数返回的闭包:

   

  makeRequire:  function (relMap, options) {
        options = options || {};
         function localRequire(deps, callback, errback) {
             var id, map, requireMod;
             if (options.enableBuildCallback && callback && isFunction(callback)) {
                callback.__requireJsBuild =  true;
            }
             if ( typeof deps === 'string') {
                 if (isFunction(callback)) {
                     //  非法调用
                     return onError(makeError('requireargs', 'Invalid require call'), errback);
                }
                 //  如果require的是require|exports|module 则直接调用handlers定义的
                 if (relMap && hasProp(handlers, deps)) {
                     return handlers[deps](registry[relMap.id]);
                }
                 if (req.get) {
                     return req.get(context, deps, relMap, localRequire);
                }
                 //  通过require的模块名Normalize成需要的moduleMap对象
                map = makeModuleMap(deps, relMap,  falsetrue);
                id = map.id;

                 if (!hasProp(defined, id)) {
                     return onError(makeError('notloaded', 'Module name "' +
                                id +
                                '" has not been loaded yet for context: ' +
                                contextName +
                                (relMap ? '' : '. Use require([])')));
                }
                 //  返回require的模块的返回值。
                 return defined[id];
            }

             //  把globalQueue 转换到 context.defQueue,并把defQueue的每一个都实例化一个context.Module对象并初始化, 放入registry数组中表示可用.
            intakeDefines();

             //  nextTick 使用的是setTimeOut.如果没有则是回调函数
             //  本次require结束后把所有deps标记为needing to be loaded.
            context.nextTick( function () {
                intakeDefines();
                requireMod = getModule(makeModuleMap( null, relMap));
                requireMod.skipMap = options.skipMap;
                requireMod.init(deps, callback, errback, {
                    enabled:  true
                });
                checkLoaded();
            });
             return localRequire;
        }
        ..
         return localRequire;

    }

如果直接require模块, 会返回此模块的返回值;否则会把他加入到context.defQueue, 初始化后等待调用; 比如直接:

     var util = require('./path/to/util'); 

会直接返回util模块返回值; 而如果:

     require(['jquery', 'backbone'], function($, Backbone){});

 

就会执行intakeDefines()nextTick();

总结

花时间读读源码,对以前使用require时的那些做法想通了,知道那样做的原因和结果,以后用着肯定也会顺手多了。

学习框架源码可以让自己用的有谱、大胆, 更多的时候是学习高手的代码组织, 编码风格,设计思想, 对自己提升帮助很大~

总结下自己研读源码方式,希望对读者有所帮助: 项目中用熟 -> 源码如何布局组织-> demo进入源码,log验证猜想-> 看别人分析 -> 总结

玉伯说 RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug, 以后一定研究下seajs,看看如何明显没有bug.