RequireJs 源碼解讀及思考

寫在前面:

最近作的一個項目,用的require和backbone,對二者的使用已經很熟悉了,可是一直都有好奇他們怎麼實現的,一直尋思着讀讀源碼。如今項目結束,終於有機會好好研究一下。javascript

本文重要解讀requirejs的源碼,backbone源碼分析將會隨後給出。html

行文思路:java

  • requirejs 基本介紹
  • requirejs使用後的幾個好奇
  • requirejs源碼解讀

requirejs基本介紹

因爲篇幅有限,這裏不會詳解requirejs的使用和api,建議讀者朋友本身去用幾回,再詳讀api.node

簡介

簡單來講,requirejs是一個遵循 AMD(Asynchronous Module Definition)規範的模塊載入框架。jquery

使用requirejs的好處是:異步加載、模塊化、依賴管理git

使用

引入:github

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

這裏的data-main是整個項目的入口.api

定義模塊:數組

   

  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-*屬性,有什麼用?
  •  Image
  • 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.

相關文章
相關標籤/搜索