JS模块化简单实现

来源:互联网 发布:淘宝退款售后订单删除 编辑:程序博客网 时间:2024/06/15 05:25

曾经花了一段时间去看RequireJS源码,虽然就两千行左右的代码,但原谅我的愚笨,真的有点看不懂。想想,还是自己实现一个再说。

简单目标

实现类似requireJS的功能,涉及到的功能

// 定义模块,三个参数define(id, deps, factory)// 执行模块,两个参数require(deps, factory) 

简单分析

先上一个简单的例子,进行分析:

require(['main'], function(main) {    console.log('start~~~');    main.hello();});
  1. 过程分析

通过deps参数,很容易知道该文件依赖的脚本是main.js,然后去加载依赖的脚本,加载完之后执行一遍并且将执行的结果作为回调函数factory的参数,然后执行该匿名的回调函数即可。

  1. 问题分析

    1. 如何加载脚本

      这个比较容易想到两个方式,一种用ajax请求,另一种用Dom元素添加让浏览器加载;由于ajax请求会有跨域问题,我们采用Dom加载的方式。

    2. 如何判断模块加载完毕

      曾经我想用script的onload事件来判断对应的模块加载,但问题在于onload只是说明了脚本已经加载,但是并没有执行。调整一下思路,定义模块的状态,在加载的时候,将状态标为加载中;记录模块的依赖数,当该模块的所有依赖全部加载完成,将执行回调函数作为该模块的对应输出,模块加载完成。

    3. 如何暂存和执行回调函数

      比如上面这个简单的例子,将回调函数作为模块main加载完毕后的回调函数即可;若依赖的模块有多个,每个依赖加载完后进行就判断依赖是否加载完毕,是就执行回调。注意,此时也意味着当前模块加载完毕,可以执行返回结果了。

    4. 循环依赖问题

      这么难的问题,留到后面再说吧!

  2. 数据结构

    首先需要一个全部的模块缓存对象,记录所有已经加载的&正在加载的模块;将模块的数据结构设计成如下:

    module[name] = {    status: String,   // 记录模块的加载状态'loading' or 'loaded'    export: Object  , // 记录该模块的输出结果    onload: Arrray[function]    // 记录模块加载完成后的回调函数}

简单实现

小女不才,实现主要参考了叶小钗博客的实现,读懂了他/她(不确定性别)的实现之后,自己实现了一遍。

// 利用闭包创建局部作用域(function(){    // 缓存所有的模块    var moduleMap = {};    //主要函数定义    function define(id, deps, factory){         var name;        //存该模块对应的参数,即factory的参数        var params = [];        if(typeof id == "object" ){ // 对应require(deps, factory)情况            factory = deps;            deps = id;        }        else{                       // 对应deine(id,deps, factory)和require(id)情况            name = id;         }        // 记录模块的依赖数        var depCount = deps && deps.length;        // 如果没有依赖,直接保存或执行模块        if( !depCount ||  depCount == 0){            return saveModule(name, null ,factory)        }        else{            deps.forEach(function(dep, i){                // 用闭包解决经典的问题                (function(i){                    // 加载依赖的模块,加载完之后的回调函数                    loadScript(dep, function(param){                        // 加载一个,计数器减一                        depCount--;                        params[i] = param;                        //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块                        if(depCount==0){                            return saveModule(name, params, factory);                        }                    })                }(i))            })        }    }    function getURL(name){        if( name.indexOf('.js') < 0){            return name + '.js';        }        return name;    }    function loadScript(name, callback){        var mod = moduleMap[name];        if( mod ){            // 如果加载完毕,直接执行回调            if( mod.status == 'loaded' ){                callback & callback(mod.export);            }            else{ // 正在加载,将回掉函数存起来                mod.onload.push(callback);            }        }        else{            // 若该模块从没加载过,定义并且利用Dom加载            moduleMap[name] = {                status: 'loading',                export: null,                onload: [callback]            }            var url = getURL(name);            var el = document.createElement('script');            el.src = url;            el.id = name;            document.body.appendChild(el);        }    }    // 保存或者执行模块    function saveModule(name, params, callback){        var mod = moduleMap[name];        if( mod ){            //设置状态为完成加载            mod.status = 'loaded';              //执行模块的函数,将结果存在export中             mod.export = callback && callback.apply(this, params);             // 执行上一级模块的回调函数            while( fn = mod.onload.pop() ){                fn && fn(mod.export);            }        }        else{            //若该模块未定义,直接执行返回结果,对应require(deps, factory)的情况            callback.apply(this, params)        }    }    // 将局部变量暴露出去    window.define = define;    window.require = define;})()

测试分析

上个简单的例子,源码Demo点击这里。

<!--index.html--><!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title> requireJS Demo -- my version </title></head><body><script src="./myRequire.js"></script> <script src="./index.js"></script> </body></html>// index.jsrequire(['main'], function(main) {    console.log('start~~~');    main.hello();});// main.js define('main', [], function() {     console.log('require module: main');     return {        hello: function() {            console.log('hello main');        }    };}); // myRequire.js// 见上一部分

下面我们来对照着分析一下执行过程,帮助理解。

  1. 入口函数require,此时name为空, depCount为1;发现依赖main,执行loadScript函数
  2. 由于moduleMap['main']未定义,定义它,并且创建Dom元素去加载main.js。此时定义如下:

    moduleMap['main'] = {    status: 'loading',    export: null,    onload:[function(param){        // depCount = 1        depCount--;        params[i] = param;        //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块        if(depCount==0){            return saveModule('', params, factory);        }    }] } 
  3. main.js加载完毕之后,执行里面的函数define('main'...).

  4. 由于该函数没有依赖,直接进入saveModule('main'...)
  5. 因为moduleMap['main']已经被定义,将状态改为loaded,执行它的回调函数,并赋值给export属性

    // 回调函数function() {     console.log('require module: main');     return {        hello: function() {            console.log('hello main');        }    };}
  6. 控制台输出'require module: main',此时的moduleMap['main']变成:

    moduleMap['main'] = {    status: 'loaded',    export: {        hello: function() {            console.log('hello main');        }    },    onload:[function(param){        // 加载一个,计数器减一        depCount--;        params[i] = param;        //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块        if(depCount==0){            return saveModule('', params, factory);        }    }] } 
  7. 执行上一级模块的回调函数moduleMap['main'].onload,将moduleMap['main'].export作为参数

  8. 此时params成[moduleMap['main'].export]depCount刚开始为1,现在为0,正好执行上一级模块的saveModule('', params, factory)
  9. 由于moduleMap['']未定义,直接执行回调函数,即:

    function(main) {    console.log('start~~~');    main.hello();}/* 其中 main =  {    hello: function() {        console.log('hello main');    }}*/

10.控制台输出'start~~~',以及'hello main'

运行到此结束。

遗留问题

很遗憾,这个实现并没解决循环依赖问题。我也看过ES6和CommonJS对循环依赖的处理,ES6使用的是引用的方式,所以不存在问题;CommonJS是只执行已经定义的部分。

幸运的是,这个实现如果有循环依赖,程序要因为不满足条件而终止执行,不会进行无数次的循环而导致崩溃。

参考资料:

  • 叶小钗 【模块化编程】理解requireJS-实现一个简单的模块加载器
  • 阮一峰 JavaScript 模块的循环加载
  • 景庄 从零开始构建JavaScript模块化加载器
0 0
原创粉丝点击