zoukankan      html  css  js  c++  java
  • 使用RequireJS并实现一个自己的模块加载器 (二)

    2017 新年好 !

    新年第一天对我来说真是悲伤 ,早上兴冲冲地爬起来背着书包跑去实验室,结果今天大家都休息 。回宿舍的时候发现书包湿了,原来盒子装的牛奶盖子松了,泼了一书包,电脑风扇口和USB口都进牛奶了,赶紧拔掉电池,用风扇吹啊吹。强行开机,无线网卡坏掉,屏幕里面进牛奶,难看死啦 ~

    鼠标也坏掉了,谁能赠送我一个鼠标啊 ..O...

    中午吃完饭,就开始完善模块加载器了。到傍晚,差不多了,出去浪了一会儿 ~

    my-Require.js

    回顾

    使用RequireJS 并实现一个自己的RequireJS(一) 中,回顾了 RequireJS 的用法,在这一篇,按照 require.js 的思路实现了一个自己的模块加载器 。

    全局变量

    首先,需要定义几个全局变量,用来保存已经加载的模块,尚未加载的模块,所有模块等全局信息。

        var context = {
            topModule: "", //顶层模块
            waitings: [], // 尚未加载的模块
            loadeds: [], // 已经加载的模块
            baseUrl: "",
    
            /**
             * 每一个模块都有下面的几个属性
             *
             * moduleNmae  模块名称
             * deps 模块依赖
             * factory 模块工厂函数 .
             * args 该模块的依赖模块的返回值
             * returnValue 该魔铠工厂函数的返回值
             */
            modules: [] // 所有模块集合
        };
    

    waitings 里面记录尚未加载的模块,但是已经插入到head中。
    loadeds 记录 已经加载完成的模块。
    在每一个script 脚本插入的onload的事件中,在waitings 里面删除模块名,在loadeds 里面添加,如果发现 waitings 为空,那么就开始递归执行工厂函数 。

    加载 require 顶层模块

    在 my-require.js 里面都是用 data-main 属性来启动 require 模块,所以先寻找 data-main 属性,并插入到 head中 。
    这里将 data-main 作为的路径作为 baseUrl ,也可以使用 requirejs.config  来配置

        var data_main_src = document.querySelector("[data-main]").getAttribute('data-main');
        // data-main 的形式为 app/main,这里 app 作为baseUrl
        var lastIndex = data_main_src.lastIndexOf("/");
        if (lastIndex === -1) {
            context.baseUrl = "./";
        } else {
            context.baseUrl = data_main_src.subString(0, lastIndex);
        }
    
        // 创建顶层节点
        var data_main_node = document.createElement('script');
        data_main_node.async = true;
        document.querySelector('head').appendChild(data_main_node);
        data_main_node.src = data_main_src + ".js";
    
        data_main_node.onload = function() {
            // 将 顶层模块 从waitings 里面杀出,并添加到 loadeds 数组中。
            removeByEle(context.waitings, context.topModule)
            context.loadeds.push(context.topModule);
        }
    
    

    这里引出第一个 辅助函数 removeByEle,这个函数用来在数组中删除元素

        /**
         * [数组根据元素删除元素]
         * @param  {[array]} arr [原数组]
         * @param  {[any]} ele [要去除的元素]
         */
        function removeByEle(arr, ele) {
            var index = arr.indexOf(ele);
            if (index === -1) return;
            arr.splice(index, 1);
        }
    

    定义 require 方法

    之后,定义require 方法。

    require 方法用来使用模块,也就是定义一个顶层模块,这个模块不需要被其他模块加载 。define 用来定义模块。
    require 可以认为是特殊的 define

    关于 require 和define 的不同,可以参见这个回答requirejs中define和require的定位以及使用区别?

    
        /**
         * [require 方法,使用一个模块]
         * @param  {[array]} deps    [依赖数组]
         * @param  {[function]} factory [工厂函数]
         */
    
       requirejs.require = function(deps, factory) {
            /**
             * 如果是 let module = require("module") 这种形式,那么就调用 use 方法。
             */
            if (arguments.length === 1 && typeof deps === "string") {
                return use(deps);
            }
    
            // 生成随机模块名,方法:
            //  callback+setTimeout("1");
            let moduleNmae = "callback" + setTimeout("1");
            context.topModule = moduleNmae;
            context.waitings.push(moduleNmae);
            // 生成一个模块配置
            context.modules[moduleNmae] = {
                moduleNmae: moduleNmae,
                deps: deps,
                factory: factory,
                args: [],
                returnValue: ""
            }
    
            // 递归遍历所有依赖,添加到 `head` 中,并设置 这个节点的一个属性`data-module-name`标识模块名。
            deps.forEach(function(dep) {
                let depPath = context.baseUrl + dep + ".js";
                let scriptNode = document.createElement('script');
                scriptNode.setAttribute("data-module-name", dep);
                scriptNode.async = true;
                scriptNode.src = depPath;
                context.waitings.push(dep);
                document.querySelector('head').appendChild(scriptNode);
                scriptNode.onload = scriptOnload;
            })
        };
    
        // 最后,把这个方法暴露给`window`
        window.require = requirejs.require;
    

    这里又引出一个函数, scriptOnload,在script 节点加载完成后触发

    `` javascript

    /**
     * [每一个脚本插入head中,都会执行这个事件 。这是事件完成两件事,
     *  1. 如果是一个匿名模块加载,那么取得这个匿名模块,并完成模块命名,
     *  2. 当节点加载完毕,判断  waitingss 是否为空,
     *  如果不为空,返回,
     *  如果为空,说明已经全部加载完毕,现在就可以执行所有的工厂函数]
     * @param  {[object]} e [事件对象]
     */
    
    function scriptOnload(e) {
        e = e || window.event;
        let node = e.target;
        let modeuleName = node.getAttribute('data-module-name');
        tempModule.modeuleName = modeuleName;
        context.modules[modeuleName] = tempModule;
        removeByEle(context.waitings, modeuleName);
        context.loadeds.push(modeuleName);
    
        if (!context.waitings.length) {
            exec(context.topModule);
        }
    }
    
    
    > 由于在define中,我使用匿名模块 。那么在define 定义一个模块的时候,无法知道模块名 。所以先放在一个`临时模块`寄存。
    > 在define 执行结束,会立即触发 `onload` 事件,那么 事件对象 `e` 就必然是这个define 正在定义的模块,就可以使用 ·`data-module-name` 取得真正
    > 的模块名,此时进行替换就可以了 。
    
    
    在看 define 如何定义一个模块
    
    ## 定义 define 方法 
    ``` javascript
       /**
         * [define 和 require 做的工作几乎相同,
         * 由于RequireJS 是更推崇匿名模块的,所以这里就没有做命名模块
         *
         * 实现匿名模块
         * 需要在这里用一个临时变量作为 doulename 保存,define 函数执行完毕,就会触发onload 事件,这个事件拿到
         * 的script节点对象必然就是这个节点,即将模块名该为正真的名字。]
         * @param  {[array]} deps    [依赖数组]
         * @param  {[function]} factory [工厂函数]
         */
        requirejs.define = function(deps, factory) {
            // 注意这里使用一个全局临时模块来标识正在加载的模块,之后来 onload 中替换为正真的模块 。
            tempModule = {
                deps: deps,
                args: [],
                returnValue: "",
                factory: factory
            }
    
            // 这里的逻辑和 require 一样,偷个懒,没有封装
            deps.forEach(function(dep) {
                var script = document.createElement("script");
                script.setAttribute("data-module-name", dep);
                script.async = true;
                script.src = baseUrl + dep + ".js";
                document.querySelector("head").appendChild(script);
                script.onload = scriptNode;
                context.waitings.push(dep);
            });
        };
        window.define = requirejs.define;
    

    执行回调

    我们再回到 scriptOnload 函数,每个模块加载完成,就会在 waitings 里面去掉,然后检查 waitings 数组,如果为空,说明全部加载完,
    就可以执行 exec 函数,在这里函数中,递归执行所有的回调 。

        /**
         * [所有模块加载完毕,递归执行工程函数 ,  核心方法]
         * @param  {[string]} moduleNmae [模块名]
         */
        function exec(moduleNmae) {
            let module = context.modules[moduleNmae];
            let deps = module.deps;
            let args = [];
            deps.forEach(function(dep) {
                exec(dep);
                args.push(context.modules[dep].returnValue);
            });
            module.args = args;
            module.returnValue = context.modules[moduleNmae].factory.apply(context.modules[moduleNmae], args);
        }
    
    

    最后走到这里,就可以发现 my-require 的核心流程了。
    先调用 require 函数和define 函数,将所有的依赖转化为script 节点插入到 dom 中,然后在 每一个 节点的 onload 事件中,将模块作为实体保存起来,并检查所有模块是否加载完成,如果加载完成,递归执行所有回调

    补充

    最后,补充两个方法

        /**
         * [ 当 形如 var module = require("module"); 这样时,就是用这个函数,最后再使用 require 重载起来,
         * 由于这个函数开始执行,那么所有的模块一定都加载完毕了,此时可以不判断waitingss,直接执行 exec ]
         * @param  {[string]} 'moduleNmae' [模块名]
         * @return {[object]}              [模块工厂函数的返回值]
         */
        function use(moduleNmae) {
            let modeule = context.modules[modeuleName];
            exec(modeuleName);
            return modeule.returnValue;
        }
    
    
        /**
         * 配置模块,这里只简单配置 baseUrl
         */ 
        requirejs.config = function(config) {
            this.context.baseUrl = config.baseUrl || baseUrl;
        }
    

    测试

    昨天太懒啦,没有编写复杂的测试,就简单的写了写,

    rect.js

    
    
    'use strict';
    
    // 这里也是一个需要改进的地方,需要判断,没有没有依赖模块
    define([], function () {
        return {
            getArea: function (length) { 
                return length * length;
            },
            getPerimeter: function (length) { 
                return length * 4;
            }
        }
    })
    
    

    circle.js

    'use strict';
    
    define([], function () {
        return {
            getArea: function (lenth) {
                return Math.PI * length * length;
            },
            getPerimeter: function (length) {
                return length * 2 * Math.PI;
            }
        }
    })
    
    

    02.js

    
    'use strict';
    
    require(["circle","rect"], function(circle,rect) {
        console.log("the area of a Circle having radius 5 is   :" + rect.getArea(5));
        console.log("the perimeter of a Rect having length 5 is   :" + circle.getPerimeter(5));
    })
    
    
    

    index.html

    <body>
        <script src="../myrequire.js" data-main="02"></script>
    </body>
    

    结果

    脚本都已经插入到 head 中了

    OK

    总结

    这个图简单地表示了测试程序的流程。(鼠标还在路上,用触摸板画的,太丑了,囧 ...等以后画一个漂亮的换上 。)

    这个模块加载器相比于 requirejs还有很多功能没有完成

    • 没有实现预加载, 这里 let module = require("module") 只能寻找到已经加载的模块。
    • 没有实现定义模块时命名
    • 缺少很多配置项
      但是,基本的功能已经实现了。
  • 相关阅读:
    4.2.1 B
    4.1.1 A
    C
    A
    排序(sort qsort)
    晕,
    clipssubviews = clipstobounds
    scrollview once more,滑出来的 刚好等于 上下偏移的,
    关于 层的显示,
    水倒过来,倒过去,穷折腾啊,
  • 原文地址:https://www.cnblogs.com/likeFlyingFish/p/6242411.html
Copyright © 2011-2022 走看看