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:
parent
6d2fc73616
commit
eeba1984a8
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user