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 相关文章推荐
Riot.js 快速的JavaScript单元测试框架
Nov 09 Javascript
js事件绑定快捷键以ctrl+k为例
Sep 30 Javascript
使用BootStrap实现用户登录界面UI
Aug 10 Javascript
Javascript 调用 ActionScript 的简单方法
Sep 22 Javascript
Bootstrap模态框(Modal)实现过渡效果
Mar 17 Javascript
jQuery 利用ztree实现树形表格的实例代码
Sep 27 jQuery
VUE 使用中踩过的坑
Feb 08 Javascript
详解ES6中的 Set Map 数据结构学习总结
Nov 06 Javascript
JavaScript对象拷贝与赋值操作实例分析
Dec 10 Javascript
关于AOP在JS中的实现与应用详解
May 06 Javascript
django js 实现表格动态标序号的实例代码
Jul 12 Javascript
vue v-model的用法解析
Oct 19 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
欧美媒体选出10年前最流行的17部动画
2017/01/18 日漫
解决PHP4.0 和 PHP5.0类构造函数的兼容问题
2013/08/01 PHP
显示程序执行时间php函数代码
2013/08/29 PHP
php中strstr、strrchr、substr、stristr四个函数的区别总结
2014/09/22 PHP
Dwz与thinkphp整合下的数据导出到Excel实例
2014/12/04 PHP
怎样搭建PHP开发环境
2015/07/28 PHP
Laravel Eloquent分表方法并使用模型关联的实现
2019/11/25 PHP
Node.js:Windows7下搭建的Node.js服务(来玩玩服务器端的javascript吧,这可不是前端js插件)
2011/06/27 Javascript
实例讲解JQuery中this和$(this)区别
2014/12/08 Javascript
JS HTML5拖拽上传图片预览
2016/07/18 Javascript
vue.js框架实现表单排序和分页效果
2017/08/09 Javascript
JS开发中基本数据类型具体有哪几种
2017/10/19 Javascript
JavaScript基础心法 数据类型
2018/03/05 Javascript
Vue-cropper 图片裁剪的基本原理及思路讲解
2018/04/17 Javascript
angular 内存溢出的问题解决
2018/07/12 Javascript
详解NodeJs项目 CentOs linux服务器线上部署
2019/09/16 NodeJs
关于vue项目中搜索节流的实现代码
2019/09/17 Javascript
从零开始用webpack构建一个vue3.0项目工程的实现
2020/09/24 Javascript
jquery实现拖拽小方块效果
2020/12/10 jQuery
Python linecache.getline()读取文件中特定一行的脚本
2008/09/06 Python
Python中的__SLOTS__属性使用示例
2015/02/18 Python
在Python中使用poplib模块收取邮件的教程
2015/04/29 Python
python 3.74 运行import numpy as np 报错lib\site-packages\numpy\__init__.py
2019/10/06 Python
详解python tkinter模块安装过程
2020/01/06 Python
HTML5 直播疯狂点赞动画实现代码 附源码
2020/04/14 HTML / CSS
荷兰在线体育用品商店:Avantisport.nl
2018/07/04 全球购物
工程管理专业个人求职信范文
2013/12/07 职场文书
五一促销活动总结
2014/07/01 职场文书
高中教师先进事迹材料
2014/08/22 职场文书
党员批评与自我批评发言材料
2014/10/14 职场文书
个人政治思想总结
2015/03/05 职场文书
2015年信访维稳工作总结
2015/04/07 职场文书
企业安全生产规章制度
2015/08/06 职场文书
宣传委员竞选稿
2015/11/19 职场文书
vue使用v-model进行跨组件绑定的基本实现方法
2021/04/28 Vue.js
Ajax请求超时与网络异常处理图文详解
2021/05/23 Javascript