1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-28 21:32:20 +00:00

docs(module-loader): ES6 循环加载 #542

This commit is contained in:
ruanyf 2017-11-12 11:52:12 +08:00
parent 6d2fc73616
commit eeba1984a8

View File

@ -449,7 +449,7 @@ CommonJS 模块加载 ES6 模块,不能使用`require`命令,而要使用`im
```javascript
// es.mjs
let foo = { bar:'my-default' };
let foo = { bar: 'my-default' };
export default foo;
foo = null;
@ -501,13 +501,13 @@ var a = require('a');
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现`a`依赖`b``b`依赖`c``c`又依赖`a`这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于JavaScript语言来说目前最常见的两种模块格式CommonJS和ES6处理“循环加载”的方法是不一样的返回的结果也不一样。
对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS ES6处理“循环加载”的方法是不一样的返回的结果也不一样。
### CommonJS模块的加载原理
### CommonJS 模块的加载原理
介绍ES6如何处理"循环加载"之前先介绍目前最流行的CommonJS模块格式的加载原理。
介绍 ES6 如何处理“循环加载”之前,先介绍目前最流行的 CommonJS 模块格式的加载原理。
CommonJS的一个模块就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
CommonJS 的一个模块,就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
```javascript
{
@ -518,9 +518,9 @@ CommonJS的一个模块就是一个脚本文件。`require`命令第一次加
}
```
上面代码就是Node内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令也不会再次执行该模块而是到缓存之中取值。也就是说CommonJS模块无论加载多少次都只会在第一次加载时运行一次以后再加载就返回第一次运行的结果除非手动清除系统缓存。
以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令也不会再次执行该模块而是到缓存之中取值。也就是说CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
### CommonJS 模块的循环加载
@ -584,9 +584,9 @@ a.js 执行完毕
exports.done = true;
```
总之CommonJS输入的是被输出值的拷贝不是引用。
总之CommonJS 输入的是被输出值的拷贝,不是引用。
另外由于CommonJS模块遇到循环加载时返回的是当前已经执行的部分的值而不是代码全部执行后的值两者可能会有差异。所以输入变量的时候必须非常小心。
另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
```javascript
var a = require('a'); // 安全的写法
@ -605,115 +605,77 @@ exports.bad = function (arg) {
### ES6 模块的循环加载
ES6 处理“循环加载”与CommonJS有本质的不同。ES6模块是动态引用如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
请看下面这个例子。
```javascript
// a.js如下
import {bar} from './b.js';
console.log('a.js');
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.js
import {foo} from './a.js';
console.log('b.js');
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
```
上面代码中,`a.js`加载`b.js``b.js`又加载`a.js`,构成循环加载。执行`a.js`,结果如下。
上面代码中,`a.mjs`加载`b.mjs``b.mjs`又加载`a.mjs`,构成循环加载。执行`a.mjs`,结果如下。
```bash
$ babel-node a.js
b.js
undefined
a.js
bar
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
```
上面代码中,由于`a.js`的第一行是加载`b.js`,所以先执行的是`b.js`。而`b.js`的第一行又是加载`a.js`,这时由于`a.js`已经开始执行了,所以不会重复执行,而是继续往下执行`b.js`,所以第一行输出的是`b.js`
上面代码中,执行`a.mjs`以后会报错,`foo`变量未定义,这是为什么?
接着,`b.js`要打印变量`foo`,这时`a.js`还没执行完,取不到`foo`的值,导致打印出来是`undefined``b.js`执行完,开始执行`a.js`,这时就一切正常了
让我们一行行来看ES6 循环加载是怎么处理的。首先,执行`a.mjs`以后,引擎发现它加载了`b.mjs`,因此会优先执行`b.mjs`,然后再执行`a.js`。接着,执行`b.mjs`的时候,已知它从`a.mjs`输入了`foo`接口,这时不会去执行`a.mjs`,而是认为这个接口已经存在了,继续往下执行。执行到第三行`console.log(foo)`的时候,才发现这个接口根本没定义,因此报错
再看一个稍微复杂的例子(摘自 Axel Rauschmayer 的[《Exploring ES6》](http://exploringjs.com/es6/ch_modules.html)
解决这个问题的方法,就是让`b.mjs`运行的时候,`foo`已经有定义了。这可以通过将`foo`写成函数来解决
```javascript
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('执行完毕');
}
foo();
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
```
按照 CommonJS 规范,上面的代码是没法执行的。`a`先加载`b`,然后`b`又加载`a`,这时`a`还没有任何执行结果,所以输出结果为`null`,即对于`b.js`来说,变量`foo`的值等于`null`,后面的`foo()`就会报错。
但是ES6可以执行上面的代码。
这时再执行`a.mjs`就可以得到预期结果。
```bash
$ babel-node a.js
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
执行完毕
// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕
```
上面代码中,`a.js`之所以能够执行原因就在于ES6加载的变量都是动态引用其所在的模块。只要引用存在代码就能执行。
下面,我们详细分析这段代码的运行过程。
这是因为函数具有提升作用,在执行`import {bar} from './b'`时,函数`foo`就已经有定义了,所以`b.mjs`加载的时候不会报错。这也意味着,如果把函数`foo`改写成函数表达式,也会报错。
```javascript
// 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();
}
}
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
```
上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。
我们再来看 ES6 模块加载器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)给出的一个例子。
```javascript
@ -722,13 +684,13 @@ import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n == 0 || odd(n - 1);
return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n != 0 && even(n - 1);
return n !== 0 && even(n - 1);
}
```
@ -751,21 +713,21 @@ true
上面代码中,参数`n`从10变为0的过程中`even()`一共会执行6次所以变量`counter`等于6。第二次调用`even()`时,参数`n`从20变为0`even()`一共会执行11次加上前面的6次所以变量`counter`等于17。
这个例子要是改写成CommonJS就根本无法执行会报错。
这个例子要是改写成 CommonJS就根本无法执行会报错。
```javascript
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
exports.even = function (n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function(n) {
module.exports = function (n) {
return n != 0 && even(n - 1);
}
```
@ -779,9 +741,9 @@ $ node
TypeError: even is not a function
```
## ES6模块的转码
## ES6 模块的转码
浏览器目前还不支持ES6模块为了现在就能使用可以将转为ES5的写法。除了Babel可以用来转码之外还有以下两个方法也可以用来转码。
浏览器目前还不支持 ES6 模块,为了现在就能使用,可以将转为 ES5 的写法。除了 Babel 可以用来转码之外,还有以下两个方法,也可以用来转码。
### ES6 module transpiler