es6 javascript的模块module(下)

来源:互联网 发布:lol一直断开网络连接 编辑:程序博客网 时间:2024/06/08 08:03

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

// a.jsvar b = require('b');// b.jsvar a = require('a');
通常,“ 循环加载” 表示存在强耦合, 如果处理不好, 还可能导致递归加载, 使得程序无法执行, 因此应该避免出现。
但是实际上, 这是很难避免的, 尤其是依赖关系复杂的大项目, 很容易出现a依赖b, b依赖c, c又依赖a这样的情况。 这意味着, 模块加载机制必须考虑“ 循环加载” 的情况。
对于 JavaScript 语言来说, 目前最常见的两种模块格式 CommonJS 和 ES6, 处理“ 循环加载” 的方法是不一样的, 返回的结果也不一样。


8.1 CommonJS 模块的加载原理

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

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

{    id: '...',    exports: {...    },    loaded: true,    ...}
上面代码就是 Node 内部加载模块后生成的一个对象。 该对象的id属性是模块名, exports属性是模块输出的各个接口, loaded属性是一个布尔值, 表示该模块的脚本是否执行完毕。 其他还有很多属性, 这里都省略了。
以后需要用到这个模块的时候, 就会到exports属性上面取值。 即使再次执行require命令, 也不会再次执行该模块, 而是到缓存之中取值。 也就是说, CommonJS 模块无论加载多少次, 都只会在第一次加载时运行一次, 以后再加载, 就返回第一次运行的结果, 除非手动清除系统缓存。


8.2 CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时执行, 即脚本代码在require的时候, 就会全部执行。 一旦出现某个模块被 " 循环加载 ",就只输出已经执行的部分, 还未执行的部分不会输出。
让我们来看, Node 官方文档里面的例子。 脚本文件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的代码。

exports.done = false;var a = require('./a.js');console.log(' 在 b.js  之中, a.done = %j', a.done);exports.done = true;console.log('b.js  执行完毕 ');
上面代码之中, b.js执行到第二行, 就会去加载a.js, 这时, 就发生了“ 循环加载”。 系统会去a.js模块对应对象的exports属性取值, 可是因为a.js还没有执行完, 从exports属性只能取回已经执行的部分, 而不是最后的值。
a.js已经执行的部分, 只有一行。

exports.done = false;
因此, 对于b.js来说, 它从a.js只输入一个变量done, 值为false。
然后, b.js接着往下执行, 等到全部执行完毕, 再把执行权交还给a.js。 于是, a.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);
执行main.js, 运行结果如下。

$ node main.js
在 b.js 之中, a.done = false
b.js 执行完毕
在 a.js 之中, b.done = true
a.js 执行完毕
在 main.js 之中, a.done = true, b.done = true
上面的代码证明了两件事。 一是, 在b.js之中, a.js没有执行完毕, 只执行了第一行。 二是, main.js执行到第二行时, 不会再次执行b.js, 而是输出缓存的b.js的执行结果, 即它的第四行。
exports.done = true;
总之, CommonJS 输入的是被输出值的拷贝, 不是引用。
另外, 由于 CommonJS 模块遇到循环加载时, 返回的是当前已经执行的部分的值, 而不是代码全部执行后的值, 两者可能会有差异。 所以, 输入变量的时候, 必须非常小心。

var a = require('a'); //  安全的写法var foo = require('a').foo; //  危险的写法exports.good = function(arg) {    return a.foo('good', arg); //  使用的是 a.foo  的最新值};exports.bad = function(arg) {    return foo('bad', arg); //  使用的是一个部分加载时的值};
上面代码中, 如果发生循环加载, require('a').foo的值很可能后面会被改写, 改用require('a') 会更保险一点。


8.3 ES6 模块的循环加载

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

// a.js 如下import {    bar} from './b.js';console.log('a.js');console.log(bar);export let foo = 'foo';// b.jsimport {    foo} from './a.js';console.log('b.js');console.log(foo);export let bar = 'bar';
上面代码中, a.js加载b.js, b.js又加载a.js, 构成循环加载。 执行a.js, 结果如下。

$ babel - node a.jsb.jsundefineda.jsbar
上面代码中, 由于a.js的第一行是加载b.js, 所以先执行的是b.js。 而b.js的第一行又是加载a.js, 这时由于a.js已经开始执行了, 所以不会重复执行, 而是继续往下执行b.js, 所以第一行输出的是b.js。
接着, b.js要打印变量foo, 这时a.js还没执行完, 取不到foo的值, 导致打印出来是undefined。 b.js执行完, 开始执行a.js, 这时就一切正常了。
再看一个稍微复杂的例子( 摘自 Dr.Axel Rauschmayer 的《 Exploring ES6》)。

    // a.jsimport {    bar} from './b.js';export function foo() {    console.log('foo');    bar();    console.log(' 执行完毕 ');}foo();// b.jsimport {    foo} from './a.js';export function bar() {    console.log('bar');    if (Math.random() > 0.5) {        foo();    }}
按照 CommonJS 规范, 上面的代码是没法执行的。 a先加载b, 然后b又加载a, 这时a还没有任何执行结果, 所以输出结果为null, 即对于b.js来说,变量foo的值等于null, 后面的foo() 就会报错。
但是, ES6 可以执行上面的代码。

$ babel - node a.jsfoobar执行完毕//  执行结果也有可能是foobarfoobar执行完毕执行完毕
上面代码中, a.js之所以能够执行, 原因就在于 ES6 加载的变量, 都是动态引用其所在的模块。 只要引用存在, 代码就能执行。
下面, 我们详细分析这段代码的运行过程。

// a.js//  这一行建立一个引用,//  从 `b.js` 引用 `bar`import {    bar} from './b.js';export function foo() {    //  执行时第一行输出 foo    console.log('foo');    //  到 b.js  执行 bar    bar();    console.log(' 执行完毕 ');}foo();// b.js//  建立 `a.js` 的 `foo` 引用import {    foo} from './a.js';export function bar() {    //  执行时,第二行输出 bar    console.log('bar');    //  递归执行 foo ,一旦随机数    //  小于等于 0.5 ,就停止执行    if (Math.random() > 0.5) {        foo();    }}
我们再来看 ES6 模块加载器 SystemJS 给出的一个例子。

// even.jsimport {    odd} from './odd'export var counter = 0;export function even(n) {    counter++;    return n == 0 || odd(n - 1);}// odd.jsimport {    even} from './even';export function odd(n) {    return n != 0 && even(n - 1);}
上面代码中, even.js里面的函数even有一个参数n, 只要不等于 0, 就会减去 1, 传入加载的odd()。 odd.js也会做类似操作。
运行上面这段代码, 结果如下。

$ babel - node > import * as m from './even.js'; > m.even(10);true> m.counter6> m.even(20)true> m.counter17
上面代码中, 参数n从 10 变为 0 的过程中, even() 一共会执行 6 次, 所以变量counter等于 6。 第二次调用even() 时, 参数n从 20 变为 0, even() 一共会执行 11 次, 加上前面的 6 次, 所以变量counter等于 17。
这个例子要是改写成 CommonJS, 就根本无法执行, 会报错。

// even.jsvar odd = require('./odd');var counter = 0;exports.counter = counter;exports.even = function(n) {        counter++;        return n == 0 || odd(n - 1);    }    // odd.jsvar even = require('./even').even;module.exports = function(n) {    return n != 0 && even(n - 1);}
上面代码中, even.js加载odd.js, 而odd.js又去加载even.js, 形成“ 循环加载”。 这时, 执行引擎就会输出even.js已经执行的部分( 不存在任何结果), 所以在odd.js之中, 变量even等于null, 等到后面调用even(n - 1) 就会报错。

$ node> var m = require('./even'); > m.even(10)TypeError: even is not afunction


9 跨模块常量

上面说过, const声明的常量只在当前代码块有效。 如果想设置跨模块的常量( 即跨多个文件), 可以采用下面的写法。

// constants.js  模块export const A = 1;export const B = 3;export const C = 4;// test1.js  模块import * as constants from './constants';console.log(constants.A); // 1console.log(constants.B); // 3// test2.js  模块import {    A,    B} from './constants';console.log(A); // 1console.log(B); // 3


10 ES6 模块的转码

浏览器目前还不支持 ES6 模块, 为了现在就能使用, 可以将转为 ES5 的写法。 除了 Babel 可以用来转码之外, 还有以下两个方法, 也可以用来转码。

10.1 ES6 module transpiler

ES6 module transpiler 是 square 公司开源的一个转码器, 可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法, 从而在浏览器中使用。
首先, 安装这个转玛器。

$ npm install - g es6 - module - transpiler
然后, 使用compile - modules convert命令, 将 ES6 模块文件转码。

$ compile - modules convert file1.js file2.js 
-o 参数可以指定转码后的文件名。

$ compile - modules convert - o out.js file1.js


10.2 SystemJS

另一种解决方法是使用 SystemJS。 它是一个垫片库( polyfill), 可以在浏览器内加载 ES6 模块、 AMD 模块和 CommonJS 模块, 将其转为 ES5 格式。 它在后台调用的是 Google 的 Traceur 转码器。
使用时, 先在网页内载入 system.js 文件。

<script src = "system.js" > < /script>
然后, 使用System.import方法加载模块文件。

<script >System.import('./app.js'); </script>
上面代码中的. / app, 指的是当前目录下的 app.js 文件。 它可以是 ES6 模块文件, System.import会自动将其转码。
需要注意的是, System.import使用异步加载, 返回一个 Promise 对象, 可以针对这个对象编程。 下面是一个模块文件。

    // app/es6-file.js:export class q {    constructor() {        this.es6 = 'hello';    }}
然后, 在网页内加载这个模块文件。

<script >    System.import('app/es6-file').then(function(m) {        console.log(new m.q().es6); // hello    }); </script>
上面代码中, System.import方法返回的是一个 Promise 对象, 所以可以用 then 方法指定回调函数。

0 0
原创粉丝点击