精华内容
下载资源
问答
  • es6 循环加载ES6模块

    2018-02-05 20:19:52
    循环加载ES6模块 “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。 // a.jsvar b = require('b');// b.jsvar a = require('a'); 通常,“循环加载”表示存在强...

    循环加载ES6模块

    “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    1. // a.js
    2. var b = require('b');
    3. // b.js
    4. var a = require('a');

    通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

    但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖bb依赖cc又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

    对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

    CommonJS模块的加载原理

    介绍 ES6 如何处理“循环加载”之前,先介绍目前最流行的 CommonJS模块格式的加载原理。

    CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

    1. {
    2. id: '...',
    3. exports: { ... },
    4. loaded: true,
    5. ...
    6. }

    上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

    以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

    CommonJS模块的循环加载

    CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

    让我们来看,Node官方文档里面的例子。脚本文件a.js代码如下。

    1. exports.done = false;
    2. var b = require('./b.js');
    3. console.log('在 a.js 之中,b.done = %j', b.done);
    4. exports.done = true;
    5. console.log('a.js 执行完毕');

    上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

    再看b.js的代码。

    1. exports.done = false;
    2. var a = require('./a.js');
    3. console.log('在 b.js 之中,a.done = %j', a.done);
    4. exports.done = true;
    5. console.log('b.js 执行完毕');

    上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

    a.js已经执行的部分,只有一行。

    1. exports.done = false;

    因此,对于b.js来说,它从a.js只输入一个变量done,值为false

    然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

    1. var a = require('./a.js');
    2. var b = require('./b.js');
    3. console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

    执行main.js,运行结果如下。

    1. $ node main.js
    2. b.js 之中,a.done = false
    3. b.js 执行完毕
    4. a.js 之中,b.done = true
    5. a.js 执行完毕
    6. main.js 之中, a.done=true, b.done=true

    上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

    1. exports.done = true;

    总之,CommonJS 输入的是被输出值的拷贝,不是引用。

    另外,由于 CommonJS模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

    1. var a = require('a'); // 安全的写法
    2. var foo = require('a').foo; // 危险的写法
    3. exports.good = function (arg) {
    4. return a.foo('good', arg); // 使用的是 a.foo 的最新值
    5. };
    6. exports.bad = function (arg) {
    7. return foo('bad', arg); // 使用的是一个部分加载时的值
    8. };

    上面代码中,如果发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。

    ES6模块的循环加载

    ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

    请看下面这个例子。

    1. // a.mjs
    2. import {bar} from './b';
    3. console.log('a.mjs');
    4. console.log(bar);
    5. export let foo = 'foo';
    6. // b.mjs
    7. import {foo} from './a';
    8. console.log('b.mjs');
    9. console.log(foo);
    10. export let bar = 'bar';

    上面代码中,a.mjs加载b.mjsb.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。

    1. $ node --experimental-modules a.mjs
    2. b.mjs
    3. ReferenceError: foo is not defined

    上面代码中,执行a.mjs以后会报错,foo变量未定义,这是为什么?

    让我们一行行来看,ES6 循环加载是怎么处理的。首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.js。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

    解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。

    1. // a.mjs
    2. import {bar} from './b';
    3. console.log('a.mjs');
    4. console.log(bar());
    5. function foo() { return 'foo' }
    6. export {foo};
    7. // b.mjs
    8. import {foo} from './a';
    9. console.log('b.mjs');
    10. console.log(foo());
    11. function bar() { return 'bar' }
    12. export {bar};

    这时再执行a.mjs就可以得到预期结果。

    1. $ node --experimental-modules a.mjs
    2. b.mjs
    3. foo
    4. a.mjs
    5. bar

    这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。

    1. // a.mjs
    2. import {bar} from './b';
    3. console.log('a.mjs');
    4. console.log(bar());
    5. const foo = () => 'foo';
    6. export {foo};

    上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。

    我们再来看 ES6模块加载器SystemJS给出的一个例子。

    1. // even.js
    2. import { odd } from './odd'
    3. export var counter = 0;
    4. export function even(n) {
    5. counter++;
    6. return n === 0 || odd(n - 1);
    7. }
    8. // odd.js
    9. import { even } from './even';
    10. export function odd(n) {
    11. return n !== 0 && even(n - 1);
    12. }

    上面代码中,even.js里面的函数even有一个参数n,只要不等于 0,就会减去 1,传入加载的odd()odd.js也会做类似操作。

    运行上面这段代码,结果如下。

    1. $ babel-node
    2. > import * as m from './even.js';
    3. > m.even(10);
    4. true
    5. > m.counter
    6. 6
    7. > m.even(20)
    8. true
    9. > m.counter
    10. 17

    上面代码中,参数n从 10 变为 0 的过程中,even()一共会执行 6 次,所以变量counter等于 6。第二次调用even()时,参数n从 20 变为 0,even()一共会执行 11 次,加上前面的 6 次,所以变量counter等于 17。

    这个例子要是改写成 CommonJS,就根本无法执行,会报错。

    1. // even.js
    2. var odd = require('./odd');
    3. var counter = 0;
    4. exports.counter = counter;
    5. exports.even = function (n) {
    6. counter++;
    7. return n == 0 || odd(n - 1);
    8. }
    9. // odd.js
    10. var even = require('./even').even;
    11. module.exports = function (n) {
    12. return n != 0 && even(n - 1);
    13. }

    上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成“循环加载”。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。

    1. $ node
    2. > var m = require('./even');
    3. > m.even(10)
    4. TypeError: even is not a function
    展开全文
  • "循环引用"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。 通常,”循环引用"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。 ...

    named import 和 default import

    一、现象

    在给公共业务组件单独打包的时候,碰到一个需要export 2个mixin和一个报错函数的场景。当时就直接这么些写的。
    // common/index.js
    // 初始化SelectMixin
        let selectUrl = '/example/common/getSelectBykeys'
        let setSelectUrlPrefix = () => {}
        
        const SelectMixin = select(selectUrl, setSelectUrlPrefix)
        
    // 初始化AuthProviderMixin
        let authUrl = '/example/common/authUrl'
        let transferAuthResult = () => {}
        const AuthProviderMixin = authProvider(authUrl, transferAuthResult)
        
    // 初始化request
        let handleRequestError = () => {}
        const RequestUtil = request(handleRequestError)
        
        export default {
            SelectMixin,
            AuthProviderMixin,
            RequestUtil
        }
    
    复制代码
    然后在一个页面引入SelectMixin,代码如下
     import { SelectMixin } from '../common/index.js'
    复制代码
    结果是提示
    "export 'SelectMixin' was not found in '../common/index.js'
    复制代码
    在控制台输出SelectMixin的时候也是undefined

    改成

     import SelectMixin from '../common/index.js'
    复制代码
    的确是有值,但是输出后发现,值的内容如下


    那么问题来了。

    二、为什么能获取到这个导出的对象却无法解构到想要到值。

    一篇简书上看到这么说的。
    export default {
        SelectMixin,
        AuthProviderMixin,
        RequestUtil
    }
    复制代码
    经过webpack 和 babel 转换成了
    module.exports.default = {
        SelectMixin,
        AuthProviderMixin,
        RequestUtil
    }
    复制代码

    所以 SelectMixin 取不到值是正常的。瞬间感觉他说的很有道理的样子。结果后面就没了后来。真是一顿操作猛如虎。。。

    后来一大佬过来了,他能懂。。。他说这是规范,就像平时的小括号和函数里的小括号一样一样的。好像是这么回事哈。。顶礼膜拜。。。

    不过最后文章中还提到了一句

    import 语句中的"解构赋值"并不是解构赋值,而是 named imports,语法上和解构赋值很像,但还是有所差别

    这里我就直接去Google “import语法上和解构赋值的差别 ”,另一名大佬就看到了 named imports ,这差距从小学语文就能看出来了。。扎心了。


    三、named imports

    在说named imported 之前先看看经常会碰到的下面的代码。这是在这个文件里写了3个函数然后导出供其他文件使用。而我们使用的时候则如右图。

    小问题: 这里引用 defaultExport 这个js 文件一定要这么写吗?

    上面的问题可以想着先,然后我们来看看下面这几个知识点之后再回来说这个问题。 上面的代码用了 export default ,而对应的import的在这个时候被称作 default import。他们是成对使用的。所以用了 export default 则一定要用到 default import。这里需要记住的知识点有以下几点。

    3.1 default exports 和 default imports

    // this is default export
    // A.js 
    export default 42
    复制代码
    // this is default import
    // B.js
    import A from './A'
    复制代码
    1. 一个模块只能有一个default exports

    2. default imports 只对 default export 有用, default exports 需要用 default imports 去获取。

    3. 在 default imports 中,导出的时候,可以为其任意命名,因为 default export 是匿名的。在下面的例子里,A, MyA, Something 都是内容相同的。只是其名字不同而已,这个语法就是获取./A值的同时,为其匿名的对象取个名字

    import A from './A'
    import MyA from './A'
    import Something from './A'
    复制代码

    这里第3点回答了上面的一个问题。这种导出的时候,是匿名的,所以引入的时候的import后面接的是为这个匿名的对象取的一个名字,这个名字是任意的。所以不用一定要取文件的名字。

    再回来看第2点。default imports 只对 default export 有用, default exports 需要用 default imports 去获取。这一句解释了在文中一开始提到问题。

    第一点:一个模块只能有一个default exports在一个文件里写多个export default 是错误的。会报错的。规则是不允许的。就记住匿名导出每个文件有且只能有一个。当然,不用匿名导出也是可以的。export default关键词后面可以跟任何值:一个函数、一个类、一个对象,所有能被命名的变量

    3.2 named exports 和 named imports

    接下来我们再来看看这两段代码。

    小问题: 这里引用 namedExport 这个js 文件一定要这么写吗?

    上面的问题可以想着先。然后我们来看看下面这几个知识点之后再回来说这个问题。上面的代码用了 named default ,而对应的import的在这个时候被称作 named import。他们是成对使用的。所以用了 named default 则一定要用到 named import。这里需要记住的知识点有以下几点。

    // this is named export
    // A.js 
    export const A = 42
    复制代码
    // this is named import
    // B.js
    import { A } from './A'
    复制代码
    1. 一个模块可以有多个 named exports
    2. named imports 只对 named export 有用, named exports 需要用 named imports 去获取
    3. 这里无法像default import 一样,给导出的对象任意取名,需要一一对应。当然可也提供了给 named import其他写法,给named export 重新命名的机会。
    // B.js
    import { A } from './A'
    import { myA } from './A' // Doesn't work!
    import { Something } from './A' // Doesn't work!
    复制代码
    • 但是一个模块导出多个named exports的时候,可以像上面那般import,不过也可以像解构一样,写在一起如下面这样。
    // A.js
    export const A = 42
    export const myA = 43
    export const Something = 44
    复制代码
    // B.js
    import { A, myA, Something } from './A'
    复制代码

    这里第3点回答了上面的一个问题。这种导出的时候,是具名的,所以要按名字去解析对应的导出。不过这种named import 给了两种其他的引入方式,可以为命名的函数重新修改名字。也可把所以具名模块合成一个对象使用。

    再回来看第2点。named imports 只对 named export 有用, named exports 需要用 named imports 去获取。这一句解释了在文中一开始提到问题。所以对于named export 只能用named import。

    你可以export任何的顶级变量:function、class、var、let、const。

    另外还发现一个有意思的,忍不住想飙一句英文: amazing ,default export 和 named export 可以混合使用,default import 和 named import 。

    在es6解构里可以用冒号为解构的变量重命名,在named import里也可以,使用的是as 。上面的代码可以这么写。
    // B.js
    import anyThing, { myA as myX, Something as XSomething } from './A'
    复制代码

    补充

    我们可以把 Default export 当成一个特殊的 named export ,其实default export 也可以像这样来解析。只是他默认是叫default的一个对象。切默认可以有任意一个名字去覆盖他的匿名。

    import { default as anything } from './A'
    复制代码

    不过这样写也是不被允许的,毕竟default 是一个保留字段。系统会直接报错,不过讲道理,抛去这个保留字段问题,这个写法按道理也能获取到default export

    import { default } from './A'
    复制代码

    四、延伸--模块的循环引用

    在了解named import 过程中碰到一个有意思的点就是 模块点循环引用。文献里说到es6 对循环引用支持比CommonJs更好。特对这个进行了一番了解。

    循环引用

    "循环引用"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    通常,”循环引用"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

    但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑”循环引用”的情况。即使在设计初期通过很好的结构设计避免了,但是代码一重构,”循环引用”还是很容易就出现的。所以”循环引用”的情况是不可能避免的。

    4.1 CommonJS模块的加载原理

    这里不讨论其他静态文件只考虑脚本文件,CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。如下:

    {
        id: '',
        exports: '',
        parent: '',
        filename: null,
        loader: false,
        children: [],
        paths: ''
        // ...
    }
    复制代码

    这个对象里,id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,省略。 具体可以去看 www.ruanyifeng.com/blog/2015/1… 也可以参考node源码 github.com/nodejs/node…

    当代码用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。

    我们先来看一个例子,考虑一下答案。

    这例子出自node官网,可以移步此处 nodejs.org/api/modules…

    不说答案,我们直接说这个在运行c.js时当过程。上边代码之中,c.js先引入了a.js。则按照required的原理,会执行a.js整个脚本。

    a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。 再看b.js的代码

    b.js执行到第二行,就会去加载a.js,这时,就发生了”循环引用”。(CommonJs的循环引用的重要原则:一旦出现某个模块被”循环引用”,就只输出已经执行的部分,还未执行的部分不会输出。) 这里有得小伙伴会认为是去执行a.js之前没执行完的代码。但是规则不是这么定义的。因为a.js触发了循环引用,则a.js会返回已经执行的部分代码。

    系统会去a.js模块对应对象的exports属性取值,a.js虽然还没有执行完,但是其exports里确实有值的,从exports属性取回已经执行的部分的值,而不是最后的值。 a.js已经执行的部分,只有一行,即 exports.done = false;

    所以对于b.js来说,引入的a.js值为false。然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。

    c.js就第一句就将a.js和b.js全部加载完毕了。a.js和b.js最终返回的都是true。

    所以最终会选择答案B。

    上面是commonjs的循环引用的原理。接下来我们再来看另外一个例子。

    例子出自此: exploringjs.com/es6/ch_modu…

    执行a.js 之后的结果是右边两种情况。这个比前面一个例子好理解。我们先来看一下es6 modules 的加载原理。

    4.2 es6 modules 的加载原理

    ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。 等到真的需要用到时,再到模块里面去取值。

    因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

    那再看上面的例子。a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

    如果按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

    4.3 CommonJs补充

    在commonJs里经常会看到exports 和 module.exports 。这个会比较混淆。

    CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)。

    在一个node执行一个文件时,会给这个文件内生成一个 exports和module对象, 而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。

    再看个例子

    从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。

    简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。


    五、所以两种循环引用的关键还是在对模块引用时的处理方式不同。

    5.1 commonJs

    • 对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。

    • 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。

    • 当使用require命令加载某个模块时,就会运行整个模块的代码。

    • 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

    • 循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

    5.2 Es6

    • ES6模块中的值属于【动态只读引用】。

    • 对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

    • 对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。

    • 循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

    参考

    www.jianshu.com/p/ba6f582d5…

    stackoverflow.com/questions/3…

    hackernoon.com/import-expo…

    2ality.com/2014/09/es6… exploringjs.com/es6/ch_modu…

    www.cnblogs.com/unclekeith/…

    www.ruanyifeng.com/blog/2015/1…

    www.ruanyifeng.com/blog/2015/0…

    github.com/nodejs/node…

    segmentfault.com/a/119000001…

    zhuanlan.zhihu.com/p/27159745

    转载于:https://juejin.im/post/5c1e58326fb9a049a570e3d5

    展开全文
  • Madge是一个开发人员工具,用于生成模块依赖关系的可视图形,查找循环依赖关系并提供其他有用信息。 乔尔·肯普(Joel Kemp)出色的用于提取依赖树。 适用于JavaScript(AMD,CommonJS和ES6模块) 也适用于CSS预...
  • 利用原生循环依赖关系序列化复杂JavaScript对象或ES6
  • value of foo: undefined foo.js:3 value of bar: This is bar.js
  • es6-circular-deps测试 测试使用ES5 require处理的循环依赖项问题与使用Babel导入ES6的情况。 作者 马克西姆·波林(Maxime Poulin)
  • 序列化本地化具有循环依赖项的复杂JavaScript对象或ES6类的序列化。 重要警告此本地插件使用的基础v8 API仍处于试验阶段,并且二进制兼容串行化将具有循环依赖关系的复杂JavaScript对象或ES6类本地化为本地序列。 ...
  • ES6的模块循环加载

    2019-09-19 18:29:06
    首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,...总是先执行依赖是正确的,但是发现循环的时候,并不会继续执行下去。 而是认为这个接口以存在 这个接口确实存在了。Module 执行分 Parse, I...

     

    首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。

    总是先执行依赖是正确的,但是发现循环的时候,并不会继续执行下去。

    而是认为这个接口以存在

    这个接口确实存在了。Module 执行分 Parse, Instantiate, Evaluate 几步。

    Parse 解析整个源代码,并收集依赖,以及所有被导入、导出的名字。

    Instantiate 递归地加载依赖(Parse + Instatntiate),并创建所有全局变量、导入符号地绑定。(至此,foo 的绑定已经存在了)注意此步并不会执行代码。
    模块间导入的绑定只知道模块间名字的对应就可以了,循环引用并不是问题,因为每个模块都有自己的导入导出的名字列表。
    全局变量绑定的时候,初始化规则与其它地方是一样的:var 初始化为 undefined ,let 不初始化,函数直接初始化为函数本身。所以,foo 的绑定是存在的,并且可以被其它导入了此符号的模块找到,但是,并没有初始化,所以不能读写。

    Evaluate 开始递归的执行代码(被导入的先执行),递归在没有依赖或遇到循环的时候停止。所以 b.mjs 先执行。a.mjs 由于还没有执行,foo 还处在没有初始化的状态(let 变量要执行到变量声明处才初始化),所有出错。

    ====================

    用现在的 node 的话,import 要写 from "./a.mjs" ,扩展名不能省略。

    然后,错误信息是:

    ReferenceError: Cannot access 'foo' before initialization

    不能访问未初始化的变量。

    展开全文
  • 在很多大的项目上面,模块使用和方法很多的情况下,特别是一些依赖关系比较复杂的大项目, 很容易会出现a依赖b,b依赖c,c又依赖a这样子的情况; 这样就会出现循环加载的情况. 这个时候就在模块(模块方法)的加载机制上面就...

    在很多大的项目上面,模块使用和方法很多的情况下,特别是一些依赖关系比较复杂的大项目, 很容易会出现a依赖b,b依赖c,c又依赖a这样子的情况; 这样就会出现循环加载的情况. 这个时候就在模块(模块方法)的加载机制上面就必须要考虑"循环加载"循环加载的情况了.

    在这篇文章里面主要说ES6和CommonJS的模块加载

    CommonJS模块的加载

    CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。然后后面,再遇到这个模块的时候就不会去重新加载而是会去内存中读取.

    {
      id: '...',
      exports: { ... },
      loaded: true,
      ...
    }
    复制代码

    在require 源码中的解读就是, 在CommonJS require进来一段脚本并且加载这个脚本的时候,就会记录一些关于这个脚本的属性, 上面的id属性是模块名,exports是模块输出的各个接口,loaded属性是布尔类型的值, 表示这个模块是否已经加载完毕.

    CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行. 如果出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。 官方文档的例子

    //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 执行完毕');
    
    //上面的代码会先执行a.js脚本,先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。
    
    //b.js
    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, 对于现在的b.js来说, a.js只是输入了一个变量done, 值为false; 然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js, 此时b.js输出的.done的变量值为true。于是,a.js接着往下执行,打印出来b.done的值为true,直到执行完毕, a输出的变量done的值为true.

    我们写一个脚本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);
    复制代码

    在当前的根目录下执行main.js 结果如下

    上面的打印结果证明了, 首先,在执行b.js的时候, a.js还没有执行完毕,只是执行了第一行;其次,main.js执行到第二行的时候,不会重新执行b.js,(因为这里没有出现重复打印),而是直接从缓存里面取出之前b.js的执行结果. 上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是只是输出缓存的b.js的执行结果(不会再次执行b.js,但是b.js中的异步代码的执行,依然会导致b.js输出接口值得变化),即它的第四行----exports.done = true;。

    ES6 循环加载

    ES模块的运行机制和CommonJS类似, 在第一次遇到一个模块的加载命令色时候, 是会立即执行的,第一次加载完成之后, 会形成一个应用, 当第二次再去加载这个模块的时候,会去读取模块里面的缓存值,不会再次重复执行这个模块里面的内容(但是由于模块内部的异步代码执行,引起缓存值的变化,读取缓存的缓存里面的值也会变化)

    请看下面的例子。

    //a.js
    console.log('a.js执行完毕')
    
    //b.js
    import * as a from './a.js'
    console.log('b.js执行完毕,a.done=%j',a.done)
    
    //main.js
    import * as a from './a.js'
    import * as b from './b.js'
    
    复制代码

    执行main.js文件,结果如下

    **- [注意!!! ] node 目前默认不支持es6 的模块 import解决方法有很多 我这里只是一种 **

    我用的是在main.js的外面包了一层,增加了一个start.js的文件, 安装了包,做了一下babel的编译,这里就简单说一下这个问题.还有其他的方法这里不细说.

    require('babel-register') ({
        presets: [ 'env' ]
    })
    module.exports = require('./main.js') 
    复制代码

    将上面CommonJS的例子改为ES的语法执行

    //a.js
    
    exports.done=false
    import * as b from './b.js'
    console.log('在 a.js 之中,b.done = %j',b.done);
    console.log('a.js 执行完毕');
    
    //b.js
    exports.done = false;
    import * as a from './a.js'
    console.log('在 b.js 之中,a.done = %j',a.done);
    exports.done = true;
    console.log('b.js执行完毕')
    
    //main.js
    import * as a from './a.js'
    import * as b from './b.js'
    console.log('在main.js中,a.done=%j,b.done=%j',a.done,b.done)
    复制代码

    执行main.js(因为在编译外面包了一层,所以执行命令是node start.js) 打印结果如下:

    执行顺序:

    首先执行a.js, 然后执行到第二行,把执行权交给了b.js, 和CommonJS的语法不同点在于, a.js没有编译完成的情况下export导出的对象都是空对象{}, 所以在这里执行到b.js的时候,打印a.done,由于a.js还没有完成编译,得到的是undefined的(CommonJS就是执行多少就暴露多少的接口,而ES6要这个文件全部编译完成才可以).

    b.js执行完毕,将执行权交给a.js,然后直到a.js执行完毕,将执行权交回给main.js.

    main.js此时打印a.done的时候,a.js已经导出完成,所以得到的是true, b.js导出完成所以得到的也是true了

    那么问题来了? ----如何解决这个ES6循环导入的问题呢, 然a.js没有编译完成也可以在另一个js文件里面导入,并使用里面的变量或者方法

    函数的提升 目前只是发现函数可以提升,如果有其他的方法,欢迎评论啊

    改写之后

    //a.js
    
    export function done(){  // 使用这个方式就不会打印出undefined 
        return false
    }
    exports.done=false
    import * as b from './b.js'
    console.log('在 a.js 之中,b.done = %j',b.done);
    console.log('a.js 执行完毕');
    
    //b.js
    exports.done = false;
    import * as a from './a.js'
    console.log('在 b.js 之中,a.done = %j',a.done());
    exports.done = true;
    console.log('b.js执行完毕')
    
    //main.js
    import * as a from './a.js'
    import * as b from './b.js'
    console.log('在main.js中,a.done=%j,b.done=%j',a.done(),b.done)
    复制代码

    执行main.js 得到结果如下:

    ###注意点

    函数提升之后,是不会挂载在a.js export导出的对象里面,但是是属于a.js里面的方法可以调用, 所以在这里去import * as a from './a.js' 的时候打印a会发现是空对象,但是调用a.done()的方法可以调用得到false这个值

    导入导出的方法有多种, import * as a from './a.js' 和 exports.done=false 写法只是其中之一,原理相同.

    ##例子 下面还有其他的几个例子大家可以打印一下

    请看下面的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)

    // a.js
    import {bar} from './b.js';
    export function foo() {
      bar();  
      console.log('执行完毕');
    }
    foo();
    
    // b.js
    import {foo} from './a.js';
    export function bar() {  
      if (Math.random() > 0.5) {
        foo();
      }
    }
    
    命令行中执行
    $ babel-node a.js
    
    执行完毕
    复制代码

    将上面例子转为CommonJS执行

    var b=require('./b.js');
    export function foo() {
      b.bar();  
      console.log('执行完毕');
    }
    foo();
    
    // b.js
    var a=require('./a.js')
    export function bar() {  
      if (Math.random() > 0.5) {
        a.foo();
      }
    }
    
    命令行中执行
    //start.js
    require('babel-register') ({
        presets: [ 'env' ]
    })
    
    module.exports = require('./a.js')   
    
    $ node start.js
    执行完毕
    
    复制代码

    //注意点 这里用到了export function... ES6的导出方式 使得变量提升, 所以这里需要配置babel转译 否则在执行到b.js的时候,会找不到foo这个方法会报出ReferenceError: foo is not defined这样的提示,这也是ES6 和 CommonJS循环加载中的我认为的最大的区别

    结语

    感谢您的来访! 如果你还希望深入的学习可以参考这里,部分参考JavaScript 模块的循环加载

    展开全文
  • es6-tutorial-源码

    2021-05-05 20:34:14
    ES6教程 课程说明: 在本视频课程中,我们将介绍...ES6#6循环的… ES6#7对象 ES6#8类 ES6#9继承 ES6#10阵列解构 ES6#11对象解构 ES6#12符号 ES6#13承诺或承诺 ES6#14异步/等待 ES6#15迭代器 ES6
  • 依赖文件地址 :https://github.com/chanceLe/ES6-Basic-Syntax/tree/master/js 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <titl...
  • es6tutorial基础入门

    2018-03-07 11:35:17
    目前看来,成千上万的Web网站依赖for-in循环,其中一些网站甚至将其用于数组遍历。如果想通过修正for-in循环增加数组遍历支持会让这一切变得更加混乱,因此,标准委员会在ES6中增加了一种新的循环语法来解决目前的...
  • ES6 语法特点import export

    千次阅读 2016-12-02 21:05:21
    博客园首页新随笔联系订阅 管理 随笔 - 256 文章 - 1 评论 - 266 ES6新特性:使用export和import实现模块化 ... 循环依赖的问题:  浏览器兼容:  参考:  在ES6前, 前端就使用RequireJS
  • ES6-Module模块

    千次阅读 2017-01-18 16:33:31
    严格模式 export 命令 import 命令 模块的整体加载 export default 命令 export 与 import 的复合写法 ...历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的
  • 使用ES6模块解决React组件依赖关系 通过browserify捆绑浏览器 CDN依赖项的填充程序通过browserify-shim 通过babelify从ES6 / JSX移植到ES5 通过gulp-uglify缩小 JSX Source Maps通过gulp-sourcemaps使用缩小的包 ...
  • ES6常用的简写技巧

    2020-11-08 22:04:31
    ES6简写技巧 初级篇 1、三目运算符 const x = 20; let answer; if(x > 10) { answer = ‘greater than 10’; }else { answer = ‘less than 10’; } 简写为: const answer = x > 10 ? ‘greater than 10’ : ...
  • CommonJS与ES6 Module的本质区别

    千次阅读 2020-07-31 17:09:55
    目录动态与静态值拷贝与动态映射循环依赖 动态与静态 动态与静态CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在...
  • Es6遍历器学习总结

    2019-04-27 17:47:09
    Es6遍历器学习总结 ...普通循环的化主要是依赖与集合的下标和长度,就是说集合是一个类数组或者数组时候,优点是使用起来简单,具体情况具体使用,遍历过程中可以 break 或者 return 退出遍历 f...
  • ES6 新的数组方法、集合、for-of 循环、展开运算符(…)甚至异步编程都依赖于迭代器(Iterator )实现。本文会详解 ES6 的迭代器与生成器,并进一步挖掘可迭代对象的内部原理与使用方法 一、迭代器的原理 在编程...
  • es6 javascript的模块module(下)

    千次阅读 2016-12-13 16:26:36
    8 循环加载“ 循环加载”( circular dependency) 指的是, a脚本的执行依赖b脚本, 而b脚本的执行又依赖a脚本。// a.js var b = require('b'); // b.js var a = require('a');通常,“ 循环加载” 表示存在强耦合...
  • 阅读目录 ES6的模块化的基本规则或特点: 下面列出几种import和export的基本语法: ES6导入的模块都是属于引用: 循环依赖的问题: 浏览器兼容: 参考: 在ES6前, 前端就使用RequireJS或者seaJS实现模块化,...

空空如也

空空如也

1 2 3 4 5
收藏数 89
精华内容 35
关键字:

es6循环依赖