requirejs 源码简析

requirejs 算是几年前一个比较经典的模块加载方案(AMD的代表)。虽然不曾用过,但它对 webpack, rollup 这些后起之秀有不少借鉴的意义,因此也决定大体分析一下它的源码进行学习。

整体结构

requirejs 首先定义了一些基本的全局变量(在requirejs自执行函数里的全局),比方说版本号,对运行环境的判断、特殊浏览器(Opera)的判断等。

其次,是定义了一系列的 util 函数,如类型判断、迭代器、对象属性判断、Mixin(掺合函数)等等。(个人感觉这些函数在版本的浏览器都有,但这里定义主要是有兼容性的考虑)。

接下来出现下面几个判断,主要是判断 define, requirejs, require 三个关键函数是否已经被定义,如果已经被定义,则不进行重写。

if (typeof define !== 'undefined') {
    //If a define is already in play via another AMD loader,
    //do not overwrite.
    return;
}

if (typeof requirejs !== 'undefined') {
    if (isFunction(requirejs)) {
        //Do not overwrite an existing requirejs instance.
        return;
    }
    cfg = requirejs;
    requirejs = undefined;
}

//Allow for a require config object
if (typeof require !== 'undefined' && !isFunction(require)) {
    //assume it is a config object.
    cfg = require;
    require = undefined;
}

然后,是一个横跨了几乎上千行的函数,newContext ,吓得我要报警了。大概浏览一下,根据注释的描述,它主要干了几件事:

  1. 定义了一些模块加载相关的变量。
  2. 定义了一些资料路径找寻的函数,如 trimDots, normalize 等。
  3. 定义了资源模块(Module)及其依赖处理相关函数。
  4. 定义了资源上下文(Context)及其加载模块的相关函数。

最后的部份,主要是定义了一些跟主入口 (main entry) 加载相关,及对外暴露的一些接口,如require, requirejs, define,分别用作加载配置、依赖加载、依赖(模块)定义。

主入口的分析与加载

在理解了整体的架构之后,我们从主入口开始,进行更深入的分析。我们可以写一个简单的 demo 进行分析,示例代码如下:

// index.html
<!DOCTYPE html>
<html>
    <head>
        <title>My Sample Project</title>
        <!-- data-main attribute tells require.js to load
             scripts/main.js after require.js loads. -->
        <script data-main="index" src="require.js"></script>
    </head>
    <body>
        <h1>My Sample Project</h1>
    </body>
</html>
// index.js
// 定义加载的配置
require.config({
    paths: {
        util: 'helper/util'
    }
});
// 主入口的加载依赖定义
requirejs(["util"], function(util) {
	console.log(util);
});

正式加载主入口文件之前,requirejs 先初始化好资源的上下文 (context),而这个时候 newContext 粉墨登场。

req = requirejs = function (deps, callback, errback, optional) {
// some other code

if (!context) {
    context = contexts[contextName] = req.s.newContext(contextName);
}

// some other code
}
// 创建默认的上下文
 req({});

打开 Chrome Debug 面板,能看到这个上下文 (context) 包含了以下一些属性与函数,默认上下文是 _,因此推断这还可以进行上下文的自定义,这个与 requirejs多版本支持有关:

下面这段源码便是寻找这个主入口文件:

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

呼应的是 html 文件里,有这么一段 js 的引用:

<script data-main="index" src="require.js"></script>

这样写, requirejs 就会帮我们去自动加载主入口文件,而它需要去提取的是 data-main 属性里的 index 值。

在最底部,有下面这段调用,requirejs 正式开始处理加载。

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

第二次调用 req 函数,之前的配置和上下文都已经配置过,因此不会再重复配置,而此时 config.deps 资源依赖已经放上了主入口文件。因此 requirejs 开始调用 context.require (localRequire) 函数进行加载。

localRequire 函数中关键用于寻找可加载模块的逻辑主要集中在这里:

//Grab defines waiting in the global queue.
intakeDefines();

//Mark all the dependencies as needing to be loaded.
context.nextTick(function() {
    //Some defines could have been added since the
    //require call, collect them.
    intakeDefines();

    requireMod = getModule(makeModuleMap(null, relMap));

    //Store if map config should be applied to this require
    //call for dependencies.
    requireMod.skipMap = options.skipMap;

    requireMod.init(deps, callback, errback, {
        enabled: true
    });

    checkLoaded();
});

intakeDefines 及里面的 takeGlobalQueue 函数,先针对 html 文件中 定义的模块进行加载,然后再调用主入口文件中定义的模块。

getModulemakeModuleMap 主要用于生成及获取模块相关的参数。

requireMod.init (Module.init) 表示需要加载的模块开始进行加载的初始化工作。在该函数里,通过 enable => check => fetch => load 几个函数之后,你会逐渐看见添加并加载 script 的逻辑。

对于主入口文件 index.js 加载 util 逻辑,原理也是大体类似。

模块的定义与加载

除了主入口以外,其它模块都会采用 define 函数进行模块的定义,下面是例子中 util 及其相关依赖模块。

// helper/utils.js
define(["helper/cart", "helper/inventory"], function(cart, inventory) {
    return {
        color: "blue",
        size: "large",
        addToCart: function() {
            inventory.decrement(this);
            cart.add(this);
        }
    }
});
// helper/inventory.js
define(function() {
    return {
        decrement: function() {

        }
    }
});
// helper/cart.js
define(function() {
    return {
        add: function() {

        }
    }
});

define 函数里面,还可以直接使用 require 进行异步加载,requirejs 除了通过第一个参数的依赖数组以外,还会通过匹配 cjsRequireRegExp 的值,进行依赖的分析,然后将相关的模块名、上下文都进行初始化:

/**
 * The function that handles definitions of modules. Differs from
 * require() in that a string for the module should be the first argument,
 * and the function to execute after dependencies are loaded should
 * return a value to define the module corresponding to the first argument's
 * name.
 */
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]);
    }
};

requirejs 加载完主入口文件之后,会开始依次加载这些依赖模块,并且也会逐个依赖模块进行依赖分析,以此类推。

点评

不过 requirejs 的缺陷还是很明显的。如果纯粹使用 requirejs 这种 AMD 的异步加载,一旦依赖非常多,就会导致加载速度很慢。当然它也提供了 r.js 这样的下构建工具,通过将文件打包到一起来解决问题,但相起比 webpackrollup,还是免不了许多手动的工作。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员宝库

给你一份详细的 Spring Boot 知识清单

在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spri...

865
来自专栏Java学习网

java开发之Java 的 Filter 过滤器学习

Filter也称之为过滤器,它是Servlet技术中最实用的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp, Serv...

591
来自专栏coding for love

JS入门难点解析2-JS的变量提升和函数提升

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

753
来自专栏CSDN技术头条

Spring Boot 完整学习指南

在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。Spring Boot 应用本质上就是一个基于 Spring 框架的应用...

682
来自专栏CSDN技术头条

给你一份超详细 Spring Boot 知识清单

在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。Spring Boot 应用本质上就是一个基于 Spring 框架的应用...

1212
来自专栏后台全栈之路

Python 调用 C 动态链接库,包括结构体参数、回调函数等

项目中要对一个用 C 编写的 .so 库进行逻辑自测。这项工作,考虑到灵活性,我首先考虑用 Python 来完成。

28011
来自专栏张善友的专栏

IBatisNet基础组件

DomSqlMapBuilder DomSqlMapBuilder,其作用是根据配置文件创建SqlMap实例。可以通过这个组件从Stream, Uri, Fil...

2255
来自专栏take time, save time

初级程序员面试不靠谱指南(四)

三、改变出生的static和extern 1.程序是怎样炼成的?IDE的发展带来的一个好处就是在写程序更多的可以关注在写这件事情上了,比如在vc里面,当你酣畅淋...

2645
来自专栏名山丶深处

Hello——Java10新特性,请了解一下

1265
来自专栏微信公众号:Java团长

Java异常进阶

在使用Java编写应用的时候,我们常常需要通过第三方类库来帮助我们完成所需要的功能。有时候这些类库所提供的很多API都通过throws声明了它们所可能抛出的异常...

764

扫码关注云+社区