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 相关文章推荐
JS动画效果代码3
Apr 03 Javascript
Javascript 页面模板化很多人没有使用过的方法
Jun 05 Javascript
JS 两日期相减,获得天数的小例子(兼容IE,FF)
Jul 01 Javascript
jquery下拉select控件操作方法分享(jquery操作select)
Mar 25 Javascript
javascript:void(0)是什么意思及href=#与href=javascriptvoid(0)的区别
Nov 13 Javascript
javascript运算符语法全面概述
Jul 14 Javascript
BootStrap Table对前台页面表格的支持实例讲解
Dec 22 Javascript
微信小程序 密码输入(源码下载)
Jun 27 Javascript
详解Vue中watch对象内属性的方法
Feb 01 Javascript
node实现socket链接与GPRS进行通信的方法
May 20 Javascript
微信小程序事件 bindtap bindinput代码实例
Aug 26 Javascript
jQuery HTML css()方法与css类实例详解
May 20 jQuery
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中通过ADO调用Asscess数据库和COM程序
2006/10/09 PHP
PHP中数组的分组排序实例
2014/06/01 PHP
php常用的url处理函数总结
2014/11/19 PHP
php实现字符串首字母转换成大写的方法
2015/03/17 PHP
php实现mysql数据库分表分段备份
2015/06/18 PHP
javascript Zifa FormValid 0.1表单验证 代码打包下载
2007/06/08 Javascript
jQuery学习2 选择器的使用说明
2010/02/07 Javascript
boxy基于jquery的弹出层对话框插件扩展应用 弹出层选择器
2010/11/21 Javascript
高性能Javascript笔记 数据的存储与访问性能优化
2012/08/02 Javascript
javascript将数组插入到另一个数组中的代码
2013/01/10 Javascript
javascript 密码框防止用户粘贴和复制的实现代码
2014/02/17 Javascript
js获取视频时长代码
2014/04/10 Javascript
javascript解析json实例详解
2014/11/05 Javascript
iframe里使用JavaScript控制主页转向的方法
2015/04/03 Javascript
纯js代码实现未知宽高的元素在指定元素中垂直水平居中显示
2015/09/12 Javascript
浅谈angular.js中实现双向绑定的方法$watch $digest $apply
2015/10/14 Javascript
Bootstrap3制作自己的导航栏
2016/05/12 Javascript
Highcharts入门之基本属性
2016/08/02 Javascript
Google Maps基础及实例解析
2016/08/06 Javascript
如何解决jQuery EasyUI 已打开Tab重新加载问题
2016/12/19 Javascript
js 监控iframe URL的变化实例代码
2017/07/12 Javascript
jQuery实现带3D切割效果的轮播图功能示例【附源码下载】
2019/04/04 jQuery
使用vscode快速建立vue模板过程详解
2019/10/10 Javascript
基于vue-draggable 实现三级拖动排序效果
2020/01/10 Javascript
Vue实现购物车实例代码两则
2020/05/30 Javascript
Python基于正则表达式实现检查文件内容的方法【文件检索】
2017/08/30 Python
Python实现采用进度条实时显示处理进度的方法
2017/12/19 Python
python 画三维图像 曲面图和散点图的示例
2018/12/29 Python
python科学计算之scipy——optimize用法
2019/11/25 Python
AmazeUI在模态框中嵌入表单形成模态输入框
2020/08/20 HTML / CSS
会计系毕业个人自荐信格式
2013/09/23 职场文书
创伤外科专业推荐信范文
2013/11/19 职场文书
大学生标准推荐信范文
2013/11/25 职场文书
教师求职自荐书
2014/06/14 职场文书
毕业生实习证明
2014/09/19 职场文书
房屋授权委托书范本
2014/10/07 职场文书