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