[源码精读系列]amd规范require.js实现细节分析

1.前言

前端模块化的框架都有自己的模块化规范。今天,我来介绍下早期传统经典的amd规范实现细节。

2.什么是amd规范

amd全称Asynchronous Module Definition(异步模块定义),由于Web网站的业务逐渐复杂,所以需要引入模块化的理念去处理业务。而如何去维护管理、加载这些模块呢?当我们在执行模块脚本的时候,传统的脚本通过同步的加载方式会阻塞脚本之后的HTML渲染,或者,一个页面上需要引入多个脚本文件,则需要写入很多次的脚本引入代码,而amd则是解决这一问题所诞生的规范。

3.Require.js的介绍

require.js是amd规范的实现,本文中的源代码版本为:RequireJS 2.3.6。下面,让我们分析学习下,它是如何实现amd规范的。

3.1 异步加载方式的处理

首先,要处理异步问题,就应该先考虑哪些问题会存在同步的情况:

1.require.js脚本自身的加载

2.require.js依赖模块的加载

3.1.1 require.js脚本自身的异步加载

 req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
        setTimeout(fn, 4);
    } : function (fn) {
        fn();
    };
    context = {
        nextTick: req.nextTick
    }
    context.nextTick(function () {
        console.log('tick')
        //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();
    });

require.js利用了setTimeout定时器的执行优先级比JS主线程优先级低的特点,解决了自身脚本的异步加载问题。

3.1.2 依赖模块异步加载

 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;
    };
  node = req.createNode(config, moduleName, url);
  node.setAttribute('data-requirecontext', context.contextName);
  node.setAttribute('data-requiremodule', moduleName);
  node.src = url;

require.js会把依赖的模块通过动态创建的脚本节点,并且设置脚本为async异步加载方式,然后再根据情况,动态的追加的html文件的头部或者其他地方。

3.2 Require.js的生命周期

上图是我整理的require.js的大致流程图,从引入require.js开始,如下所示:

 <script type="text/javascript" data-main="./a" src="require.js"></script>

require.js通过读取data-main标签的属性,来确定入口脚本信息cfg。然后再执行req(cfg)进行引入。在这之前,会有一次req({})初始化的工作,具体实现如下:

 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;
            }
        });
    }
req处理主脚本

后续通过context.nextTick定时器来加载模块,模块的加载方式,我刚开始的时候已经介绍了。define定义的依赖,最终会入栈到依赖队列中,当加载完最后一个脚本文件时,先执行主脚本,然后按序回溯调用。

4.简易版require.js的实现

(function(global){
    var context = {
        exec: [],
        current_id: undefined,
        define: function(deps, callback) {
           context.localRequire(deps, callback);
        },
        require: function(deps, callback) {
            context.localRequire(deps, callback);
        },
        localRequire: function(deps, callback) {
            let mod = context.makeModule(deps);
            context.exec.push({id:context.current_id, fn: callback});
            if (-1 === context.exec.findIndex((item)=>mod.id === item.id)) {
                context.createNode(mod);
            }
        },
        makeRequire: function() {
            let main = document.getElementsByTagName('script')[0].getAttribute('data-main');
            let module = this.makeModule(main);
            this.current_id = module.id;
            this.createNode(module);
        },
        createNode: function(deps) {
            var node = document.createElement('script');
            node.async = true;
            node.src = deps.src;
            node.setAttribute('module_id', deps.id);
            var head = document.getElementsByTagName('head')[0];
            head.appendChild(node);
            node.addEventListener('load', function(event){
                if (event.type === 'load') {
                    let module_id = this.getAttribute('module_id');
                    context.current_id = module_id;
                    context.execMod();
                }
            });
        },
        makeModule: function(deps) {
            return {
                id: deps.substring(deps.indexOf('/') + 1),
                src: deps + '.js'
            }
        },
        execMod: function() {
           while(this.exec.length) {
                let script = this.exec.pop();
                setTimeout(() => {
                    script && script.fn();
                }, 4)
           }
        }
    }
    global.define = context.define;
    global.require = context.require;
    setTimeout(()=> context.makeRequire(), 4);
})(window)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" data-main="./b" src="require.js"></script>
</head>
<body>
<div></div>
</body>
</html>
define('c', function(){
    console.log('b')
})
require('d', function(){
    console.log('c')
})
//循环引用c
require('c', function(){
    console.log('d')
})

console.log('d2')
执行结果

5.参考

https://requirejs.org

如无特殊说明,文章均为本站原创,转载请注明出处。如发现有什么不对的地方,希望得到您的指点。

发表评论

电子邮件地址不会被公开。 必填项已用*标注