JavaScript 模块的循环加载实现方法


Posted in Javascript onDecember 13, 2015

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

// a.js
var b = require('b');

// b.js
var a = require('a');

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

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

本文介绍JavaScript语言如何处理"循环加载"。目前,最常见的两种模块格式CommonJS和ES6,处理方法是不一样的,返回的结果也不一样。

一、CommonJS模块的加载原理

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

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

{
 id: '...',
 exports: { ... },
 loaded: true,
 ...
}

上面代码中,该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。(详细介绍情参见《require() 源码解读》。)

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

二、CommonJS模块的循环加载

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

让我们来看,官方文档里面的例子。脚本文件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;

三、ES6模块的循环加载

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

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

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了500毫秒,又变为等于baz。

让我们看看,m2.js能否正确读取这个变化。

$ babel-node m2.js

bar
baz

面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

请看下面的例子(摘自 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();
 }
}

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

但是,ES6可以执行上面的代码。

$ babel-node a.js

执行完毕

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

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

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
 counter++;
 return n == 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
 return n != 0 && even(n - 1);
}

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

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

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

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

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

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
 counter++;
 return n == 0 || odd(n - 1);
}

// odd.js
var 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 a function

[说明] 本文是我写的《ECMAScript 6入门》第20章《Module》中的一节。

Javascript 相关文章推荐
用javascript实现自定义标签
May 08 Javascript
比Jquery的document.ready更快的方法
Apr 28 Javascript
js鼠标左右键 键盘值小结
Jun 11 Javascript
js控制的回到页面顶端goTop的代码实现
Mar 20 Javascript
捕获浏览器关闭、刷新事件不同情况下的处理方法
Jun 02 Javascript
jquery对table中各数据的增加、保存、删除操作示例
May 14 Javascript
Bootstrap每天必学之附加导航(Affix)插件
Apr 25 Javascript
js字符串操作总结(必看篇)
Nov 22 Javascript
JQuery页面随滚动条动态加载效果的简单实现(推荐)
Feb 08 Javascript
javascript修改浏览器title方法 JS动态修改浏览器标题
Nov 30 Javascript
vue组件name的作用小结
May 23 Javascript
javascript操作元素的常见方法小结
Nov 13 Javascript
javascript日期验证之输入日期大于等于当前日期
Dec 13 #Javascript
详解JavaScript正则表达式之RegExp对象
Dec 13 #Javascript
详解JavaScript基于面向对象之继承
Dec 13 #Javascript
轻松使用jQuery双向select控件Bootstrap Dual Listbox
Dec 13 #Javascript
基于jQuery通过jQuery.form.js插件实现异步上传
Dec 13 #Javascript
推荐阅读的js快速判断IE浏览器(兼容IE10与IE11)
Dec 13 #Javascript
JS如何判断是否为ie浏览器的方法(包括IE10、IE11在内)
Dec 13 #Javascript
You might like
PHP中substr()与explode()函数用法分析
2014/11/24 PHP
windows下配置php5.5开发环境及开发扩展
2014/12/25 PHP
PHP汉字转换拼音的函数代码
2015/12/30 PHP
PHP记录页面停留时间的方法
2016/03/30 PHP
详解在YII2框架中使用UEditor编辑器发布文章
2018/11/02 PHP
Laravel关系模型指定条件查询方法
2019/10/10 PHP
js的写法基础分析
2011/01/17 Javascript
JQuery入门——用映射方式绑定不同事件应用示例
2013/02/05 Javascript
简单的ajax连接库分享(不用jquery的ajax)
2014/01/19 Javascript
用循环或if语句从json中取数据示例
2014/08/18 Javascript
基于jquery实现页面滚动时顶部导航显示隐藏
2020/04/20 Javascript
AngularJS中处理多个promise的方式
2016/02/02 Javascript
Taro集成Redux快速上手的方法示例
2018/06/21 Javascript
巧妙运用v-model实现父子组件传值的方法示例
2019/04/07 Javascript
新手简单了解vue
2019/05/29 Javascript
微信小程序使用Vant Weapp组件库的方法步骤
2019/08/01 Javascript
Javascript查看大图功能代码实现
2020/05/07 Javascript
js异步接口并发数量控制的方法示例
2020/11/22 Javascript
[02:45]DOTA2英雄敌法师基础教程
2013/11/25 DOTA
学习python (2)
2006/10/31 Python
python 转换 Javascript %u 字符串为python unicode的代码
2016/09/06 Python
python list元素为tuple时的排序方法
2018/04/18 Python
python如何爬取动态网站
2020/09/09 Python
利用CSS3伪元素实现逐渐发光的方格边框
2017/05/07 HTML / CSS
澳大利亚便宜隐形眼镜购买网站:QUICKLENS Australia
2018/10/06 全球购物
双立人美国官方商店:ZWILLING集团餐具和炊具
2020/05/07 全球购物
实习护士自我鉴定
2013/10/13 职场文书
统计学专业毕业生的自我评价分享
2013/11/28 职场文书
2014植树节活动总结
2014/03/11 职场文书
聚美优品广告词改编
2014/03/14 职场文书
简单租房协议书范本
2014/08/20 职场文书
小学毕业典礼演讲稿
2014/09/09 职场文书
公司委托书格式范文
2014/10/09 职场文书
2014年招商工作总结
2014/11/22 职场文书
2015年元旦促销方案书
2014/12/09 职场文书
Linux中如何安装并部署Redis
2022/04/18 Servers