【源碼學習】之requirejs

對於如今的前端生態來講,requirejs是有點過期了,webpack幫咱們包乾了一切。可是對於學習源碼這件事情來講,永遠是不過期的!javascript

最近稍微閒下來了一點,就着之前作過的項目,我也來看看requirejs的源碼。但願能漲點姿式!html

1.html中的data-main是個什麼鬼?

//address.html
<
script type="text/javascript" data-main="${base}/static/js/app/userCenter/address" src="${base}/static/js/plugins/require.js"></script>

使用requirejs,在咱們的頁面須要引入一個有data-main的主入口js文件。前端

既然這樣,咱們就去require源碼中去找找data-main在哪裏出現了。java

//Look for a data-main script attribute, which could also adjust the baseUrl.去尋找一個data-main的script屬性,而且可以匹配baseUrl
    if (isBrowser && !cfg.skipDataMain) {
        //Figure out baseUrl. Get it from the script tag with require.js in it.計算出baseUrl.從含有require.js的script標籤中獲取它.
        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;
            }
        });
    }

 咱們在源碼中找到了6處匹配的地方,所有在上面這段代碼中.webpack

這裏用到了一個公有方法eachReverse,包含兩個參數,ary和func,func是回調函數,回調函數接受三個參數(數組的每一項,數組的索引,完整的數組元素).web

  /**
     * Helper function for iterating over an array backwards. If the func 幫助函數爲了倒序遍歷數組,若是func返回true,則跳出循環
     * returns a true value, it will break out of the loop.
     */
    function eachReverse(ary, func) {
        if (ary) {
            var i;
            for (i = ary.length - 1; i > -1; i -= 1) {
                if (ary[i] && func(ary[i], i, ary)) {
                    break;
                }
            }
        }
    }

 eachReverse的ary是一個scripts()方法返回的數組。因此接下來去看看scripts方法。scripts方法取到html上全部的script標籤.編程

 function scripts() {
        return document.getElementsByTagName('script');
    }

 經過script取到data-main屬性的值。咱們能夠看到dataMain變量的值就是address.html中data-main屬性的值。數組

接下來的操做都是對url地址的一些處理.經過src.pop()取得mainScript的值爲address.閉包

 

再將地址拼接起來取得子目錄。能夠看到subPath少了前面的/address目錄,subPath被賦值給了cfg.baseUrl屬性。app

 jsSuffixRegExp = /\.js$/,

//Strip off any trailing .js since mainScript is now
//like a module name.
//剝去任何.js結尾的mainScript,使得它看起來像一個模塊的名稱
 mainScript = mainScript.replace(jsSuffixRegExp, '');

 

經過正則匹配任何已.js結尾的文件。例如上面的address.html的data-main若是變成:xxxxx/address.js ,這裏就會把.js給替換掉,如同註釋中字面意義的「模塊化」。

到這裏的話,對data-main的處理算完結了。正如data-main是咱們的主模塊,address.html的主模塊就是deps裏的address。

可是要說一點的就是這裏的cfg對象是要在req({});初始化執行上下文之後纔會須要用到。這裏只是按照咱們正常思惟打斷點先想到的。

2.js裏面怎麼跑

註釋上寫到這裏是程序的主入口,至關於構造函數,那咱們就來看一下。

 1 /**
 2      * Main entry point.主入口
 3      *
 4      * If the only argument to require is a string, then the module that
 5      * is represented by that string is fetched for the appropriate context.
 6      *
 7      * If the first argument is an array, then it will be treated as an array
 8      * of dependency string names to fetch. An optional function callback can
 9      * be specified to execute when all of those dependencies are available.
10      *
11      * Make a local req variable to help Caja compliance (it assumes things  建立一個局部req變量去幫助caja compliance,這個caja貌似說的是一個google的caja庫,相似建立了一個虛擬的iframe,而且給一個短名稱的局部做用域去使用。
12      * on a require that are not standardized), and to give a short
13      * name for minification/local scope use.
14      */
15     req = requirejs = function (deps, callback, errback, optional) {
16 
17         //Find the right context, use default
18         var context, config,
19             contextName = defContextName;
20 
21         // Determine if have config object in the call.
22         if (!isArray(deps) && typeof deps !== 'string') {
23             // deps is a config object            deps是一個配置對象
24             config = deps;
25             if (isArray(callback)) {         
26                 // Adjust args if there are dependencies
27                 deps = callback;
28                 callback = errback;
29                 errback = optional;
30             } else {
31                 deps = [];
32             }
33         }
34 
35         if (config && config.context) {
36             contextName = config.context;
37         }
38 
39         context = getOwn(contexts, contextName);
40         if (!context) {
41             context = contexts[contextName] = req.s.newContext(contextName); 
42         }
43 
44         if (config) {
45             context.configure(config);
46         }
47 
48         return context.require(deps, callback, errback);
49     };

 在隨後的代碼中,執行了req而且傳入一個空對象,這裏就建立了req這個函數執行的上下文。

//Create default context.
req({});

 

 

這裏用到了getOwn函數,getOwn要配合hasProp使用。先檢查是否包含實例屬性,若是包含的話就將屬性賦值到目標對象。

1  function hasProp(obj, prop) {
2         return hasOwn.call(obj, prop);
3     }
4 
5  function getOwn(obj, prop) {
6         return hasProp(obj, prop) && obj[prop];
7     }

由於context爲false,因此newContext進行了初始化。

1  s = req.s = {
2         contexts: contexts,
3         newContext: newContext
4     };

 newContext的代碼很是的多,差很少1500行左右。

newContext大體結構以下:

1.一些工具方法:例如trimDots。

2.處理模塊的方法:例如normalize等

3.建立並保存了require的運行環境:context對象中的方法

4.建立了require的模塊:Module構造函數

 這裏context對象調用了makeRequire方法。

 context.require = context.makeRequire();
 return context;
1 //簡化後的代碼,能夠很明顯的看出,爲了造成閉包
2 makeRequire:function(){
3   function localRequire(){
4       //TODO
5       return localRequire;  
6    } 
7     return localRequire;   
8 }

經過一個mixin方法實現了屬性拷貝。

 1  /**
 2      * Simple function to mix in properties from source into target, 簡單的方法把源對象的屬性混合進目標對象中,僅在目標對象並無相同屬性名稱的狀況下
 3      * but only if target does not already have a property of the same name.
 4      */
 5     function mixin(target, source, force, deepStringMixin) {
 6         if (source) {
 7             eachProp(source, function (value, prop) {
 8                 if (force || !hasProp(target, prop)) {
 9                     if (deepStringMixin && typeof value === 'object' && value &&
10                         !isArray(value) && !isFunction(value) &&
11                         !(value instanceof RegExp)) {
12 
13                         if (!target[prop]) {
14                             target[prop] = {};
15                         }
16                         mixin(target[prop], value, force, deepStringMixin);
17                     } else {
18                         target[prop] = value;
19                     }
20                 }
21             });
22         }
23         return target;
24     }

 最後返回的target,也就是咱們localRequire,添加了4個屬性,這裏咱們能夠看出來,它是返回了函數localRequire的閉包。

又給localRequire這個閉包再添加了一個屬性,undef

並將閉包賦值給context.require。隨後返回context這個對象。

 

而後咱們會進入configure這個方法,由於第一次初始化是傳入的一個空對象,因此這裏對配置的處理並無什麼實際意義,咱們暫且略過。在第二次有具體參數傳入了再具體說明。

最後將在context對象中維護的localRequire閉包執行並返回。

return context.require(deps, callback, errback);

咱們會碰到nextTick這樣一個方法,req.nextTick將匿名函數添加到事件隊列中去,異步的去執行它,而這裏的匿名函數的功能就是去異步的加載require的模塊。可是爲什麼這裏與前一次異步延時設置爲4,我以爲1,2,3應該都是能夠的,這裏不是很清楚!若是有朋友瞭解,能夠解釋一下
不過這裏的註釋仍是很可笑的:若是有比setTimeout更好的方法,那麼就去重寫它。而後用的名稱叫nextTick,就是在Node中爲了解決setTimeout存在問題的方法。你們有興趣的話能夠去看看《異步編程》。

1 /**
2      * Execute something after the current tick
3      * of the event loop. Override for other envs
4      * that have a better solution than setTimeout.
5      * @param  {Function} fn function to execute later.
6      */
7     req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
8         setTimeout(fn, 4);
9     } : function (fn) { fn(); };

 繼續往下走,咱們看到了經過mixin方法添加到閉包的4個屬性,這裏把這4個屬性給暴露給了外層的req對象。

 1  //Exports some context-sensitive methods on global require.
 2     each([
 3         'toUrl',
 4         'undef',
 5         'defined',
 6         'specified'
 7     ], function (prop) {
 8         //Reference from contexts instead of early binding to default context,
 9         //so that during builds, the latest instance of the default context
10         //with its config gets used.
11         req[prop] = function () {
12             var ctx = contexts[defContextName];
13             return ctx.require[prop].apply(ctx, arguments);
14         };
15     });

 隨後會執行我前面提到的處理data-main這塊的代碼。當全部的準備工做作好了之後,

在這裏就將咱們前面經過data-main拿到的cfg對象傳進去。

1  //Set up with config info.
2  req(cfg);

3.小結一下

req({}) => req(cfg);

這一段流程走過之後,咱們發現最大的改變就是contexts這個對象。

=>

而這些改變最重要的目的就是建立一個適合require運行的上下文環境。固然經過makeRequire建立的閉包函數ocalRequire,它也是不一樣的,由於後面的邏輯不一樣,傳入的參數不一樣,造成了不一樣的閉包。

這幾天require讀下來,感受沒那麼好懂,果真仍是水平不夠,先好好消化一下。下次再來繼續啃. 

相關文章
相關標籤/搜索