zoukankan      html  css  js  c++  java
  • RequireJs 源码解读及思考

    写在前面:

    最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码。现在项目结束,终于有机会好好研究一下。

    本文重要解读requirejs的源码,backbone源码分析将会随后给出。

    行文思路:

    • requirejs 基本介绍
    • requirejs使用后的几个好奇
    • requirejs源码解读

    requirejs基本介绍

    由于篇幅有限,这里不会详解requirejs的使用和api,建议读者朋友自己去用几次,再详读api.

    简介

    简单来说,requirejs是一个遵循 AMD(Asynchronous Module Definition)规范的模块载入框架。

    使用requirejs的好处是:异步加载、模块化、依赖管理

    使用

    引入:

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

    这里的data-main是整个项目的入口.

    定义模块:

       

      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 = /(/*([sS]*?)*/|([^:]|^)//(.*)$)/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.

  • 相关阅读:
    javaEE_maven_struts2_tomcat_first
    企业框架-Spring
    MyBatis延迟加载及缓存
    MyBatis注解及动态Sql
    框架之MyBatis
    SQL中的一些关键字用法
    Mysql————基本sql语句
    表单验证
    java中的锁——列队同步器
    线程同步Lock锁
  • 原文地址:https://www.cnblogs.com/freestyle21/p/4457511.html
Copyright © 2011-2022 走看看