• 【源码学习】之requirejs


    对于现在的前端生态来说,requirejs是有点过时了,webpack帮我们包干了一切。但是对于学习源码这件事情来说,永远是不过时的!

    最近稍微闲下来了一点,就着以前做过的项目,我也来看看requirejs的源码。希望能涨点姿势!

    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在哪里出现了。

    //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处匹配的地方,全部在上面这段代码中.

    这里用到了一个公有方法eachReverse,包含两个参数,ary和func,func是回调函数,回调函数接受三个参数(数组的每一项,数组的索引,完整的数组元素).

      /**
         * 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属性。

     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读下来,感觉没那么好懂,果然还是水平不够,先好好消化一下。下次再来继续啃. 

  • 相关阅读:
    微信小程序开发之普通链接二维码
    微信小程序之使用本地接口开发
    c# partial 分部类和分部方法
    .NET之美 第一部分C#语言基础
    Head First设计模式之命令模式
    Head First设计模式之责任链模式
    Head First设计模式之解释器模式
    Head First设计模式之迭代器模式
    Head First设计模式之中介者模式
    LeetCode 709. To Lower Case
  • 原文地址:https://www.cnblogs.com/ChengWuyi/p/6813903.html
Copyright © 2020-2023  润新知