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 相关文章推荐
设置下载不需要倒计时cookie(倒计时代码)
Nov 19 Javascript
JQuery 选择和过滤方法代码总结
Nov 19 Javascript
jQuery仿Excel表格编辑功能的实现代码
May 01 Javascript
DOM基础教程之事件对象
Jan 20 Javascript
Express实现前端后端通信上传图片之存储数据库(mysql)傻瓜式教程(二)
Dec 10 Javascript
JavaScript操作表单实例讲解(上)
Jun 20 Javascript
AngularJS表单详解及示例代码
Aug 17 Javascript
微信 java 实现js-sdk 图片上传下载完整流程
Oct 21 Javascript
javascript基础知识之html5轮播图实例讲解(44)
Feb 17 Javascript
深入理解vue-loader如何使用
Jun 06 Javascript
详解基于node的前端项目编译时内存溢出问题
Aug 01 Javascript
ES6/JavaScript使用技巧分享
Dec 14 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模块化安装教程
2016/06/01 PHP
动态加载图片路径 保持JavaScript控件的相对独立性
2010/09/03 Javascript
JavaScript中利用各种循环进行遍历的方式总结
2015/11/10 Javascript
全面解析Bootstrap中form、navbar的使用方法
2016/05/30 Javascript
js判断文件格式及大小的简单实例(必看)
2016/10/11 Javascript
Javascript中字符串replace方法的第二个参数探究
2016/12/05 Javascript
jQueryUI 拖放排序遇到滚动条时有可能无法执行排序的小bug及解决方案
2016/12/19 Javascript
Bootstrap源码解读表单(2)
2016/12/22 Javascript
jQuery动态追加页面数据以及事件委托详解
2017/05/06 jQuery
QRCode.js:基于JQuery的生成二维码JS库的使用
2017/06/23 jQuery
微信小程序支付及退款流程详解
2017/11/30 Javascript
Vue.js中使用iView日期选择器并设置开始时间结束时间校验功能
2018/08/12 Javascript
浅谈Vue.js 中的 v-on 事件指令的使用
2018/11/25 Javascript
小程序开发踩坑:页面窗口定位(相对于浏览器定位)(推荐)
2019/04/25 Javascript
Node 代理访问的实现
2019/09/19 Javascript
跟老齐学Python之使用Python查询更新数据库
2014/11/25 Python
对于Python中RawString的理解介绍
2016/07/07 Python
深入理解Python装饰器
2016/07/27 Python
Python3实现带附件的定时发送邮件功能
2020/12/22 Python
python基于物品协同过滤算法实现代码
2018/05/31 Python
pandas重新生成索引的方法
2018/11/06 Python
python模块导入的细节详解
2018/12/10 Python
快速解决pyqt5窗体关闭后子线程不同时退出的问题
2019/06/19 Python
Pandas中resample方法详解
2019/07/02 Python
基于Python的微信机器人开发 微信登录和获取好友列表实现解析
2019/08/21 Python
Python多线程:主线程等待所有子线程结束代码
2020/04/25 Python
卡西欧G-SHOCK英国官网: 防水防震手表
2018/01/08 全球购物
数以千计的折扣工业产品:ESE Direct
2018/05/20 全球购物
馥蕾诗美国官网:Fresh美国
2019/10/09 全球购物
结构工程研究生求职信
2013/10/13 职场文书
人民教师的自我评价分享
2014/02/21 职场文书
机电专业大学生职业规划书范文
2014/02/25 职场文书
人力资源经理的岗位职责范本
2014/02/28 职场文书
2015年安康杯竞赛活动总结
2015/03/26 职场文书
诗词赏析-(浣溪沙)
2019/08/13 职场文书
Python自动化爬取天眼查数据的实现
2021/06/15 Python