IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    javascript模块机制原理

    Reverland (lhtlyy@gmail.com)发表于 2015-05-02 00:00:00
    love 0

    eloquent javascript是一本浸润着黑客精神和文化的书,上一次看到这样的书还是在三年前,那本书叫land of lisp。

    这是关于eloquent js第十章,模块化的一些解释。因为我觉得这部分不好理解。

    js模块化基础

    我们写代码时,代码总是倾向于越来越像浆糊,越是大的全的功能,越是浆糊到不堪。我们想要看清楚些,就把不同功能分出来,揉成一堆小浆糊,这总比一大团浆糊好处理。

    当我们想把一团js浆糊放到一起时,并称之模块时,我们会设计让它提供几个功能,这个一般叫做接口。比如console模块有个log功能,比如等等。

    我们可以把这堆浆糊扔到一个全局变量中去,这样其它部分要是想要使用这团浆糊的功能,就使用浆糊提供的接口。比如Math.PI可以访问得到3.14159……。

    这很简单,js提供了函数来隔离命名空间,对象来放置模块内容。

    var mod1 = function mod1(){
        var i = 3;
        function print(x){
            console.log(x+i);
        }
        return {a:1,
                b:print
        };
    }()

    为啥要这样写呢,因为,假如我们不想让人看到局部变量i,函数是我们唯一能借以创建局部作用域的东西。

    这就是javascript模块化的基础。

    这样,我们想调用某个模块时,就把某个函数包裹着的东西给全局变量,调用者对这个全局变量进行操作就好。

    return的时候写一大堆对象内容也不合适,我们可以选择传进去个对象。

    var mod2 = {};
    (function(exports){
        var i = 3;
        exports.a = i;
        exports.b = function print(x){
            console.log(x+i);
        };
    }(mod2))

    但是。。。

    当这所有都得需要在全局作用域内进行。

    1. 想想当我们要两个模块a和b都被c依赖,a依赖c0.1版而b依赖c0.2版,a和b中调用名字为c的模块。。。

    2. 或者a依赖b然后c依赖d,然而b在a中命名为xx,d在c中也命名为xx。

    所以,最好不通过全局作用域实现模块依赖。

    但实际上可以做到不需要全局作用域来实现模块的依赖.接下来讨论两种常见的方案。

    向CommonJS跃进

    写过node程序的人都见过类似的东西

    var mod3 = require("mod3");

    在该模块中通过require函数引入模块,并通过变量mod3引用这个模块。不需要通过全局变量,该模块高明地引用了其它模块。

    require实现方式如下, 通过Function构造函数构造函数实现命名空间, 假设我们有个read函数。

    function require(modName) {
        var code = new Function("exports", read(modName));
        var exports = {};
        code(exports);
        return exports;
    }

    这样做,每次载入都会运行模块,即使有多个模块载入一个名字的模块也会运行多次。 我们加个全局变量保存已经加载的模块。

    require.cache = Object.create(null);
    function require(modName) {
        if (modName in require.cache) {
            return require.cache[modName];
        }
        var code = new Function("exports", read(modName));
        var exports = {};
        code(exports);
        require.cache[modName] = exports;
        return exports;
    }

    在比如你想暴露个和exports对象不同的东西,比如我他妈的只想导出个函数呢,比如

    var fn = require('fn');
    console.log(fn);    //-> 1

    我们可以通过额外给模块传递一个叫module的参数,这个参数exports属性默认指向exports对象实现这点。

    require.cache = Object.create(null);
    function require(modName) {
        if (modName in require.cache) {
            return require.cache[modName];
        }
        var code = new Function("exports, module", read(modName));
        var exports = {};
        var module = {exports: exports};
        code(exports, module);
        require.cache[modName] = module.exports;
        return module.exports;
    }

    当模块fn想返回比如1时

    module.exports = 1;

    这样,我们就实现了简单的nodejs模块系统:)Coooooooooooooooool

    这有个啥问题呢?浏览器中的js程序执行时,浏览器啥也干不了= =。

    read函数没读到模块内容之前,js程序一直执行,但除了等待什么都不干。假如这个read是从网络上读取模块文件,那么万一网络质量很差,这个系统都把大部分时间花在等文件加载上了。

    为了解决这个问题,有人发明了browserify.这,看做一个依赖打包服务吧。

    另一种方案是:

    AMD

    这里的AMD不是AMD芯片的AMD,全称叫Asynchronous Module Definition。异步模块定义模块系统。

    这个系统的核心,是一个叫做define的函数。

    每个模块都必须这样写:

    define(["dep1", "dep2"], function(dep1, dep2) {
        return dep1.a + dep2.b;
    })

    假如不依赖其它模块

    define([], function() {
        var mod = {a: 1, b: function(){console.log("sb")}};
        return mod;
    })

    这个核心的define函数这么设计,

    function define(depNames, moduleFunction) {
        //对每个depNames中的依赖,安排异步下载
        //当下载都完成时,执行moduleFunction, 同时把模块接口传给它
        //改变其状态,通知调用者
    }

    我们要实现这个递归的过程,需要一个对象来表示其状态和存放调用者的函数

    var defineCache = Object.create(null);
    // 指向当前模块的指针
    var currentMod = null;
    function getModule(name) {
        //如果已经加载过了就返回
        if (name in defineCache)
            return defineCache[name];
        // 否则先返回一个对象
        // 等模块真正下载完后更新currentMod变量,
        // 同时递归执行调用子模块的define函数
        var module = {exports: null,
            loaded: false,
            onLoad: []};
        defineCache[name] = module;
        // 我们假设有这么个异步读取文件的函数
        backgroundReadFile(name, function(code) {
                currentMod = module;
                new Function("", code)();// code会是又一个define函数调用
                });
        return module;
    }
    function define(depNames, moduleFunction) {
        //对每个depNames中的依赖,安排异步下载
        //当下载都完成时,执行moduleFunction, 同时把模块接口传给它
        //改变其状态,通知调用者
        var myMod = currentMod;
        var deps = depNames.map(getModule);
    
        deps.forEach(function(mod) {
                if (!mod.loaded)
                // 如果模块还没加载把父模块的whenDepsLoaded保存
                // 留待该模块完成以后调用
                mod.onLoad.push(whenDepsLoaded);
                });
    
        function whenDepsLoaded() {
            //如果依赖没有全加载好,值得一提的是[].every总是返回真
            if (!deps.every(function(m) { return m.loaded; }))
                return;
            // 如果依赖都下载完成,如果deps为[],args=[]
            var args = deps.map(function(m) { return m.exports; });
            var exports = moduleFunction.apply(null, args);
            if (myMod) {    //对当前模块对象进行更新
                myMod.exports = exports;
                // 更新当前模块状态
                myMod.loaded = true;
                //当前模块完成时都会调用一次依赖它的模块们的whenDepsLoaded函数
                myMod.onLoad.forEach(function(f) { f(); });
            }
        }
        whenDepsLoaded();
    }

    结果就是:

    1. 首先调用顶级define,define中所有依赖调用getModule去下载被依赖者代码,被依赖者的代码下载完成后会执行下一个define。

    2. define中getModule会立即返回一个对象,这个对象保存想要加载的被依赖模块的导出接口、是否完成加载信息,和依赖它的模块的whenDepsLoaded函数。

    3. 该模块调用其whenDepsLoade函数,该函数在依赖没有全加载完时立即返回。

    4. 接下来就等待被依赖模块下载好,被依赖函数又是一个define函数。define函数重复上述过程,

    5. 此递归过程继续。直到某个没有依赖的模块

    6. 对没有依赖的模块,define中直接调用whenDepsLoaded函数,更新它的导出接口,更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。(注意js的函数作用域中的myMod)

    7. 该whenDepsLoaded函数保存了它自身的模块名和信息。如果它还有其它依赖没加载,立即返回。直到它所有依赖的模块的状态都变了,它的whenDepsLoaded函数才从此真正有了实质作用。把加载好的被依赖模块作为参数,开始真正执行模块代码(之前早就下载好的define的一部分)。之后更新它的导出接口、更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。

    8. 被依赖的模块完成后又重复过程7,不断调用更高级别的依赖者的whenDepsLoaded函数,直到所有的函数都执行完。顶级的define中的whenDepsLoaded执行完。

    著名的require.js的设计就是这个原理。

    我的逻辑性有点浆糊,但我觉得每种情况都说明白了,没有依赖,依赖其它,不被依赖的模块。

    接口设计原则

    1. 可预测:不总做出乎意料的设计。
    2. 组件化:尽可能功能通用,提供简单的数据结构和语法。
    3. 分层设计:暴露不同程度的细节。

    综上,这都是些原理和基本原则。实际会涉及很多复杂的问题。不过,万丈高楼平地起,浮沙之上无高台,基础是深入的前提。而这个前提确是:万事开头难。

    感谢看完的读者,希望以后碰到模块化问题都能更轻松迅速解决。



沪ICP备19023445号-2号
友情链接