zoukankan      html  css  js  c++  java
  • JS模块之AMD, CMD, CommonJS、UMD和ES6模块

    CommonJS

    传送门

    同步加载,适合服务器开发,node实现了commonJS。module.exports和require

    判断commonJS环境的方式是(参考jquery源码):

    if ( typeof module === "object" && typeof module.exports === "object" ) 

     一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

    // a.js
    exports.done = false;
    var b = require('./b.js');
    console.log('在 a.js 之中,b.done = %j', b.done);
    exports.done = true;
    console.log('a.js 执行完毕');
    
    // b.js
    exports.done = false;
    var a = require('./a.js');
    console.log('在 b.js 之中,a.done = %j', a.done);
    exports.done = true;
    console.log('b.js 执行完毕');
    
    // main.js
    var a = require('./a.js');
    var b = require('./b.js');
    console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
    
    // 运行结果
    在 b.js 之中,a.done = false
    b.js 执行完毕
    在 a.js 之中,b.done = true
    a.js 执行完毕
    在 main.js 之中, a.done=true, b.done=true
    View Code

    对于commonJs模块的运行理解:

      模块中的代码运行在一个函数中,所有根上的变量都存在于当前作用域中,然后返回一个导出对象,导出对象中的函数引用的都是当前作用域内的变量。 

    UMD

    UMD是AMD和CommonJS的糅合

    AMD模块以浏览器第一的原则发展,异步加载模块。
    CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
    这迫使人们又想出另一个更通用的模式UMD (Universal Module Definition)。希望解决跨平台的解决方案。

    UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。
    在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

    ES6模块

      模块内默认开启严格模式,可用于代替AMD和commonJs,也就是说不再须要UMD了。ES6中对于AMD异步加载使用的是import函数,而commonJs同步加载对应ES6中import关键字

      export和import要求必须在顶层作用域中,import在静态编译阶段执行,不能使用运行时的值。但import()函数可以,他异步加载返回一个promise,能运行在任意地方,可以实现按需下载

    和commonJs的差异

      commonJs输出的是一个对象;而ES6模块输出的不是对象,而是一个只读地址引用的集合。前者在生成对象返回的时候,会对对象中要用到的值进行拷贝进对象中,这很常规容易理解,而后者输出的是地址,所以在运行时会动态地到模块的作用域中读取值,没有“拷贝”这种说法,如果对es6导出的值进行赋值,报错:

    Uncaught TypeError: Assignment to constant variable.

    但可以调用导出函数,函数内对模块内的值进行修改。

      因为es6模块默认是严格模式,所以顶层this为undefined,而commonJs中顶层this指向当前模块

    不同地方多次导入同一个模块,模块只会执行一次:

    // x.js
    import {c} from './mod';
    c.add();  // 0 -> 1
    
    // y.js
    import {c} from './mod';
    c.show(); //1
    
    // main.js
    import './x';
    import './y';
    
    //运行main 输出 1

    以上不同模块中都导入了一次mod模块,而获取到的导出对象是相同的,说明mod模块只运行了一次。

    循环引用

       当模块被执行的时候,会首先执行被提升了的语句(functnion、export),在执行import,最后再执行内部的赋值语句、逻辑等。

      出现循环引用时,只要被引入模块已经运行完还没运行完,就不会再次运行。a->b->a,b中的a就不会再次运行。

    // a.mjs
    import {bar} from './b';
    console.log('a.mjs');
    console.log(bar);
    export let foo = 'foo';
    
    // b.mjs
    import {foo} from './a';
    console.log('b.mjs');
    console.log(foo);
    export let bar = 'bar';
    
    // 运行a
    b.mjs
    ReferenceError: foo is not defined

    把a中foo的赋值改为函数定义,则不会报错,因为函数定义有提升。

    babel的转换原理

    export { NormalExport }
    export { RenameExport as HasRenamed }
    export default DefaultExport
    
    // 转换为
    exports.NormalExport = NormalExport;
    exports.HasRenamed = RenameExport;
    exports.default = DefaultExport;
    
    //-------------------------------------------
    
    import { NormalExport } from 'normal'
    
    // 转换为
    var _normal = require('normal');

    可见,import关键字是同步的。

    Chrome 61 加入了对 JavaScript Module <script type="module"> 的原生支持,后续介绍以下这个特性的用法:

    参考:http://es6.ruanyifeng.com/#docs/module-loader#加载规则

    在script中加入type=module,意味着这个脚本支持ES6模块的语法。

    <script type="module">
            import {x} from './es6module.js';
            alert(x);
    </script>
    
    //./es6module.js
    export var x = "123";

    以上能正常运行。如果去掉type,则报错:Uncaught SyntaxError: Unexpected token import

    加了这个属性,两段脚本就运行在不同的环境中了,以下运行报错 ReferenceError: x is not defined

        <script type="module">
            var x = 123
        </script>
        <script>
            alert(x);
        </script>

    总结ES6模块和commonJs的对比:

    1. 导出的机制不同,前者是只读的地址,而后者是一个对象
    2. 顶层this的指向不同
    3. 循环引用时,都是遇到被加载的模块正在运行或者已经运行完,就不再进去运行,模块没运行完输出的都是已运行部分。
    4. es6静态编译,import关键字不允许使用动态值,而commonJs的require是动态执行,可以读取动态值

    AMD

    可实现异步的模块加载。适合浏览器,RequireJS库实现了AMD规范  传送门

    关键词:define(factory)、require(['xxx'],fn)  

    补充:单文件多模块

    2.js

    define('a',function(){
        return {
            v:"a"
        }
    });
    
    define('b',function(){
        return {
            v:"b"
        }
    });

    main.js

    require.config({
        paths: {
            'a': "2",
            'b': "2",
        }
    });
    
    require(['a','b'], function(a,b) {
        alert(a.v);
        alert(b.v);
    });

    可见,paths中的名字不是随便写的,必须和模块文件内定义的名字要一致

    测试2:多次引入同一个模块

    2.js

    define('a',function(){
        return {
            v:"a"
        }
    });
    console.log('module a exec')

    main.js

    require.config({
        paths: {
            'a': "2",
        }
    });
    
    require(['a'], function(a) {
        a.x = 789;
        require(['a' + ""], function(a) {
            console.log(a.x);
        });
        console.log(123)
    });

    输出

    多次引入只会执行一次模块,而且使用的都是同一个模块对象,虽然第二次引入的模块在第一次就下载好了,但回调的代码依然会在下一轮事件循环中执行

    模块的名字可以是表达式,如以上的['a'] 可以写成['a'+""],而且仅当require被执行的时候,模块才会开始下载,下载完再执行模块,然后触发回调函数。

    实际做的事情:

    1. require函数检查依赖的模块,根据配置文件,获取js文件的实际路径
    2. 根据js文件实际路径,在dom中插入script节点,并绑定onload事件来获取该模块加载完成的通知。
    3. 依赖script全部加载完成后,调用回调函数

    CMD

    传送门

    seaJS实现了这个规范。分析阶段(使用正则表达式进行依赖的抓取),会把所有require的文件都下载好了(这一步可能会导致程序刚开始响应比较慢),再执行入口模块

    所以我们使用的时候写 var a = require('a') 虽然是引入一个模块,但可以认为这句代码没有任何阻塞,因为模块已经提前下载好了

     测试代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Event Propagation</title>
        <script src="https://cdn.bootcss.com/seajs/3.0.2/sea.js"></script>
        <script>
            seajs.use("./1.js")
        </script>
    </head>
    <body>
    </body>
    </html>
    
    // 1.js
    define(function(require){
        console.log("main module exec")
        setTimeout(function(){
            var exec = Math.random()*100%2;
            if(exec > 1){
                let two = require("./2.js");
                let two1 = require("./2.js");
                two.show();
            }
        },1000)
    });
    
    // 2.js
    define(function(require,exports,module){
        console.log("module two exec")
        module.exports = {
            show :function(){
                console.log("show in module two exec")
            }
        }
    })

     运行结果:

    结论:

      seajs在分析阶段会下载所有的依赖而不管这些依赖是否会被真正执行,依赖的名字可以使用变量,依赖都下载好了才执行入口模块,依赖在第一次被require的时候才执行

    实际做的事情是:

    1. 通过回调函数的Function.toString函数,使用正则表达式来捕捉内部的require字段,找到require('jquery')内部依赖的模块jquery
    2. 根据配置文件,找到jquery的js文件的实际路径
    3. 在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上
    4. 回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数
    5. 当回调函数调用require('jquery'),即执行绑定在'jquery'这个id上的js文件,即刻执行,并将返回值传给var b
    可见以上依赖搜索这一块用正则表达式搜索,导致模块名只能硬编码,而不能运算和使用变量

    rj和sj的区别

      https://github.com/seajs/seajs/issues/277

      https://www.zhihu.com/question/20351507

      https://www.zhihu.com/question/20342350

      都是异步下载模块。

      rj会按需下载,下载完马上执行,不能保证多个依赖是按顺序执行的,次序不可控,玉伯认为这是一个坑,但是amd规定中从来没规定过模块的执行次序,异步模块之间可以没有先后,但被依赖的异步模块必须先于当前模块执行

      sj会先进行分析,然后下载好所有依赖,再开始执行入口模块(这可能会导致程序响应比较慢);在模块第一次被require的时候,模块才会执行,所以可以保证模块的执行次序和require的次序是一致的,但这可能导致一个问题,模块被require的时候执行,万一内部出错了,当前模块该怎么办?被依赖的模块有问题,当前模块执行有何意义?

      对于cmd,一require就可以马上使用,给人的感觉就像是同步代码,而amd逻辑是写在回调函数中的,给人异步的感觉

  • 相关阅读:
    CKEditor配置
    Asp.net中防止用户多次登录的方法【转】
    Android sharedUserId研究记录
    Android Wifi模块学习
    你应该知道的asp.net 之 服务器端包括指令
    钻牛角尖之try return finally
    .NET集合总结
    web debugger fiddler 使用小结
    钻牛角尖之Request.Cookies与Response.Cookies
    speeding up your web site 前端性能优化规则(一)
  • 原文地址:https://www.cnblogs.com/hellohello/p/7589184.html
Copyright © 2011-2022 走看看