javascript模块机制原理
eloquent javascript是一本浸润着黑客精神和文化的书,上一次看到这样的书还是在三年前,那本书叫land of lisp。
这是关于eloquent js第十章,模块化的一些解释。因为我觉得这部分不好理解。
js模块化基础
我们写代码时,代码总是倾向于越来越像浆糊,越是大的全的功能,越是浆糊到不堪。我们想要看清楚些,就把不同功能分出来,揉成一堆小浆糊,这总比一大团浆糊好处理。
当我们想把一团js浆糊放到一起时,并称之模块时,我们会设计让它提供几个功能,这个一般叫做接口。比如=console=模块有个=log=功能,比如等等。
我们可以把这堆浆糊扔到一个全局变量中去,这样其它部分要是想要使用这团浆糊的功能,就使用浆糊提供的接口。比如=Math.PI=可以访问得到3.14159……。
这很简单,js提供了函数来隔离命名空间,对象来放置模块内容。
var mod1 = function mod1(){ var i = 3; function print(x){ console.log(x+i); } return {a:1, b:print }; }()
为啥要这样写呢,因为,假如我们不想让人看到局部变量=i=,函数是我们唯一能借以创建局部作用域的东西。
这就是javascript模块化的基础。
这样,我们想调用某个模块时,就把某个函数包裹着的东西给全局变量,调用者对这个全局变量进行操作就好。
=return=的时候写一大堆对象内容也不合适,我们可以选择传进去个对象。
var mod2 = {}; (function(exports){ var i = 3; exports.a = i; exports.b = function print(x){ console.log(x+i); }; }(mod2))
但是。。。
当这所有都得需要在全局作用域内进行。
- 想想当我们要两个模块a和b都被c依赖,a依赖c0.1版而b依赖c0.2版,a和b中调用名字为c的模块。。。
- 或者a依赖b然后c依赖d,然而b在a中命名为xx,d在c中也命名为xx。
所以,最好不通过全局作用域实现模块依赖。
但实际上可以做到不需要全局作用域来实现模块的依赖.接下来讨论两种常见的方案。
向CommonJS跃进
写过node程序的人都见过类似的东西
var mod3 = require("mod3");
在该模块中通过require函数引入模块,并通过变量mod3引用这个模块。不需要通过全局变量,该模块高明地引用了其它模块。
require实现方式如下, 通过=Function=构造函数构造函数实现命名空间, 假设我们有个read函数。
function require(modName) { var code = new Function("exports", read(modName)); var exports = {}; code(exports); return exports; }
这样做,每次载入都会运行模块,即使有多个模块载入一个名字的模块也会运行多次。 我们加个全局变量保存已经加载的模块。
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对象不同的东西,比如我他妈的只想导出个函数呢,比如
var fn = require('fn'); console.log(fn); //-> 1
我们可以通过额外给模块传递一个叫module的参数,这个参数=exports=属性默认指向=exports=对象实现这点。
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时
module.exports = 1;
这样,我们就实现了简单的nodejs模块系统:)Coooooooooooooooool
这有个啥问题呢?浏览器中的js程序执行时,浏览器啥也干不了= =。
read函数没读到模块内容之前,js程序一直执行,但除了等待什么都不干。假如这个read是从网络上读取模块文件,那么万一网络质量很差,这个系统都把大部分时间花在等文件加载上了。
为了解决这个问题,有人发明了browserify.这,看做一个依赖打包服务吧。
另一种方案是:
AMD
这里的AMD不是AMD芯片的AMD,全称叫Asynchronous Module Definition。异步模块定义模块系统。
这个系统的核心,是一个叫做define的函数。
每个模块都必须这样写:
define(["dep1", "dep2"], function(dep1, dep2) { return dep1.a + dep2.b; })
假如不依赖其它模块
define([], function() { var mod = {a: 1, b: function(){console.log("sb")}}; return mod; })
这个核心的define函数这么设计,
function define(depNames, moduleFunction) { //对每个depNames中的依赖,安排异步下载 //当下载都完成时,执行moduleFunction, 同时把模块接口传给它 //改变其状态,通知调用者 }
我们要实现这个递归的过程,需要一个对象来表示其状态和存放调用者的函数
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(); }
结果就是:
- 首先调用顶级define,define中所有依赖调用getModule去下载被依赖者代码,被依赖者的代码下载完成后会执行下一个define。
- define中getModule会立即返回一个对象,这个对象保存想要加载的被依赖模块的导出接口、是否完成加载信息,和依赖它的模块的whenDepsLoaded函数。
- 该模块调用其whenDepsLoade函数,该函数在依赖没有全加载完时立即返回。
- 接下来就等待被依赖模块下载好,被依赖函数又是一个define函数。define函数重复上述过程,
- 此递归过程继续。直到某个没有依赖的模块
- 对没有依赖的模块,define中直接调用whenDepsLoaded函数,更新它的导出接口,更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。(注意js的函数作用域中的myMod)
- 该whenDepsLoaded函数保存了它自身的模块名和信息。如果它还有其它依赖没加载,立即返回。直到它所有依赖的模块的状态都变了,它的whenDepsLoaded函数才从此真正有了实质作用。把加载好的被依赖模块作为参数,开始真正执行模块代码(之前早就下载好的define的一部分)。之后更新它的导出接口、更新它的加载状态,调用依赖它的模块的whenDepsLoaded函数。
- 被依赖的模块完成后又重复过程7,不断调用更高级别的依赖者的whenDepsLoaded函数,直到所有的函数都执行完。顶级的define中的whenDepsLoaded执行完。
著名的require.js的设计就是这个原理。
我的逻辑性有点浆糊,但我觉得每种情况都说明白了,没有依赖,依赖其它,不被依赖的模块。
接口设计原则
- 可预测:不总做出乎意料的设计。
- 组件化:尽可能功能通用,提供简单的数据结构和语法。
- 分层设计:暴露不同程度的细节。
综上,这都是些原理和基本原则。实际会涉及很多复杂的问题。不过,万丈高楼平地起,浮沙之上无高台,基础是深入的前提。而这个前提确是:万事开头难。
感谢看完的读者,希望以后碰到模块化问题都能更轻松迅速解决。