盒子
盒子
文章目录
  1. js模块化基础
  2. 向CommonJS跃进
  3. AMD
  4. 接口设计原则

Javascript模块机制原理

eloquent javascript是一本浸润着黑客精神和文化的书,上一次看到这样的书还是在三年前,那本书叫land of lisp。

这是关于eloquent js第十章,模块化的一些解释。因为我觉得这部分不好理解。

js模块化基础

我们写代码时,代码总是倾向于越来越像浆糊,越是大的全的功能,越是浆糊到不堪。我们想要看清楚些,就把不同功能分出来,揉成一堆小浆糊,这总比一大团浆糊好处理。

当我们想把一团js浆糊放到一起时,并称之模块时,我们会设计让它提供几个功能,这个一般叫做接口。比如console模块有个log功能,比如等等。

我们可以把这堆浆糊扔到一个全局变量中去,这样其它部分要是想要使用这团浆糊的功能,就使用浆糊提供的接口。比如Math.PI可以访问得到3.14159……。

这很简单,js提供了函数来隔离命名空间,对象来放置模块内容。

1
2
3
4
5
6
7
8
9
var mod1 = function mod1(){
var i = 3;
function print(x){
console.log(x+i);
}
return {a:1,
b:print
};
}()

为啥要这样写呢,因为,假如我们不想让人看到局部变量i,函数是我们唯一能借以创建局部作用域的东西。

这就是javascript模块化的基础。

这样,我们想调用某个模块时,就把某个函数包裹着的东西给全局变量,调用者对这个全局变量进行操作就好。

return的时候写一大堆对象内容也不合适,我们可以选择传进去个对象。

1
2
3
4
5
6
7
8
var mod2 = {};
(function(exports){
var i = 3;
exports.a = i;
exports.b = function print(x){
console.log(x+i);
};
}(mod2))

但是。。。

当这所有都得需要在全局作用域内进行。

  1. 想想当我们要两个模块a和b都被c依赖,a依赖c0.1版而b依赖c0.2版,a和b中调用名字为c的模块。。。

  2. 或者a依赖b然后c依赖d,然而b在a中命名为xx,d在c中也命名为xx。

所以,最好不通过全局作用域实现模块依赖。

但实际上可以做到不需要全局作用域来实现模块的依赖.接下来讨论两种常见的方案。

向CommonJS跃进

写过node程序的人都见过类似的东西

1
var mod3 = require("mod3");

在该模块中通过require函数引入模块,并通过变量mod3引用这个模块。不需要通过全局变量,该模块高明地引用了其它模块。

require实现方式如下, 通过Function构造函数构造函数实现命名空间, 假设我们有个read函数。

1
2
3
4
5
6
function require(modName) {
var code = new Function("exports", read(modName));
var exports = {};
code(exports);
return exports;
}

这样做,每次载入都会运行模块,即使有多个模块载入一个名字的模块也会运行多次。
我们加个全局变量保存已经加载的模块。

1
2
3
4
5
6
7
8
9
10
11
require.cache = Object.create(null);
function require(modName) {
if (modName in require.cache) {
return require.cache[modName];
}
var code = new Function("exports", read(modName));
var exports = {};
code(exports);
require.cache[modName] = exports;
return exports;
}

在比如你想暴露个和exports对象不同的东西,比如我他妈的只想导出个函数呢,比如

1
2
var fn = require('fn');
console.log(fn); //-> 1

我们可以通过额外给模块传递一个叫module的参数,这个参数exports属性默认指向exports对象实现这点。

1
2
3
4
5
6
7
8
9
10
11
12
require.cache = Object.create(null);
function require(modName) {
if (modName in require.cache) {
return require.cache[modName];
}
var code = new Function("exports, module", read(modName));
var exports = {};
var module = {exports: exports};
code(exports, module);
require.cache[modName] = module.exports;
return module.exports;
}

当模块fn想返回比如1时

1
module.exports = 1;

这样,我们就实现了简单的nodejs模块系统:)Coooooooooooooooool

这有个啥问题呢?浏览器中的js程序执行时,浏览器啥也干不了= =。

read函数没读到模块内容之前,js程序一直执行,但除了等待什么都不干。假如这个read是从网络上读取模块文件,那么万一网络质量很差,这个系统都把大部分时间花在等文件加载上了。

为了解决这个问题,有人发明了browserify.这,看做一个依赖打包服务吧。

另一种方案是:

AMD

这里的AMD不是AMD芯片的AMD,全称叫Asynchronous Module Definition。异步模块定义模块系统。

这个系统的核心,是一个叫做define的函数。

每个模块都必须这样写:

1
2
3
define(["dep1", "dep2"], function(dep1, dep2) {
return dep1.a + dep2.b;
})

假如不依赖其它模块

1
2
3
4
define([], function() {
var mod = {a: 1, b: function(){console.log("sb")}};
return mod;
})

这个核心的define函数这么设计,

1
2
3
4
5
function define(depNames, moduleFunction) {
//对每个depNames中的依赖,安排异步下载
//当下载都完成时,执行moduleFunction, 同时把模块接口传给它
//改变其状态,通知调用者
}

我们要实现这个递归的过程,需要一个对象来表示其状态和存放调用者的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var defineCache = Object.create(null);
// 指向当前模块的指针
var currentMod = null;
function getModule(name) {
//如果已经加载过了就返回
if (name in defineCache)
return defineCache[name];
// 否则先返回一个对象
// 等模块真正下载完后更新currentMod变量,
// 同时递归执行调用子模块的define函数
var module = {exports: null,
loaded: false,
onLoad: []};
defineCache[name] = module;
// 我们假设有这么个异步读取文件的函数
backgroundReadFile(name, function(code) {
currentMod = module;
new Function("", code)();// code会是又一个define函数调用
});
return module;
}
function define(depNames, moduleFunction) {
//对每个depNames中的依赖,安排异步下载
//当下载都完成时,执行moduleFunction, 同时把模块接口传给它
//改变其状态,通知调用者
var myMod = currentMod;
var deps = depNames.map(getModule);

deps.forEach(function(mod) {
if (!mod.loaded)
// 如果模块还没加载把父模块的whenDepsLoaded保存
// 留待该模块完成以后调用
mod.onLoad.push(whenDepsLoaded);
});

function whenDepsLoaded() {
//如果依赖没有全加载好,值得一提的是[].every总是返回真
if (!deps.every(function(m) { return m.loaded; }))
return;
// 如果依赖都下载完成,如果deps为[],args=[]
var args = deps.map(function(m) { return m.exports; });
var exports = moduleFunction.apply(null, args);
if (myMod) { //对当前模块对象进行更新
myMod.exports = exports;
// 更新当前模块状态
myMod.loaded = true;
//当前模块完成时都会调用一次依赖它的模块们的whenDepsLoaded函数
myMod.onLoad.forEach(function(f) { f(); });
}
}
whenDepsLoaded();
}

结果就是:

  1. 首先调用顶级define,define中所有依赖调用getModule去下载被依赖者代码,被依赖者的代码下载完成后会执行下一个define。

  2. define中getModule会立即返回一个对象,这个对象保存想要加载的被依赖模块的导出接口、是否完成加载信息,和依赖它的模块的whenDepsLoaded函数。

  3. 该模块调用其whenDepsLoade函数,该函数在依赖没有全加载完时立即返回。

  4. 接下来就等待被依赖模块下载好,被依赖函数又是一个define函数。define函数重复上述过程,

  5. 此递归过程继续。直到某个没有依赖的模块

  6. 对没有依赖的模块,define中直接调用whenDepsLoaded函数,更新它的导出接口,更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。(注意js的函数作用域中的myMod)

  7. 该whenDepsLoaded函数保存了它自身的模块名和信息。如果它还有其它依赖没加载,立即返回。直到它所有依赖的模块的状态都变了,它的whenDepsLoaded函数才从此真正有了实质作用。把加载好的被依赖模块作为参数,开始真正执行模块代码(之前早就下载好的define的一部分)。之后更新它的导出接口、更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。

  8. 被依赖的模块完成后又重复过程7,不断调用更高级别的依赖者的whenDepsLoaded函数,直到所有的函数都执行完。顶级的define中的whenDepsLoaded执行完。

著名的require.js的设计就是这个原理。

我的逻辑性有点浆糊,但我觉得每种情况都说明白了,没有依赖,依赖其它,不被依赖的模块。

接口设计原则

  1. 可预测:不总做出乎意料的设计。
  2. 组件化:尽可能功能通用,提供简单的数据结构和语法。
  3. 分层设计:暴露不同程度的细节。

综上,这都是些原理和基本原则。实际会涉及很多复杂的问题。不过,万丈高楼平地起,浮沙之上无高台,基础是深入的前提。而这个前提确是:万事开头难。

感谢看完的读者,希望以后碰到模块化问题都能更轻松迅速解决。