如何实现一个CMD规范的模块加载器
/ / 点击 / 阅读耗时 10 分钟why
最初原因是作为面试准备的主题之一,后来发现研究原理最好的方法就是自己动手实现一个。
what
前端早已进入了模块化开发的时代,Node.js火起来后,CommonJS作为服务端js模块化的标准。与此同时,针对浏览器端的模块化规范AMD, CMD陆续推出,其中最具代表性的就是require.js和sea.js。
how
如何定位一个模块
每个module都有一个ID(一般是模块的相对路径),根据传入的baseURL+模块ID得到该模块的完整路径。
如何加载一个模块
得到模块的完整路径后,通过动态创建script脚本来加载模块:
1 | var node = document.createElement('script'); |
如何获取一个模块的依赖
每加载一个模块,都会在内部维护一个模块依赖映射表:
1 | { |
这样通过模块id很容易得到模块对应的依赖信息,找到依赖后,就可以通过id定位到模块的完整路径,然后通过完整路径去加载这个模块了,也就是重复加载单个模块的过程。
模块重复加载怎么办
开发过程中,很容易遇到A模块依赖B模块,C模块也依赖B模块的场景。如果按照上面的模块加载流程,B模块会被加载两次,也就是在分别加载A模块和C模块的时候,都会加载B。很明显这是一种资源上面的浪费,明明已经加载过了,为嘛还要加载一遍。
解决方法很简单,每次加载依赖模块前先看看依赖映射表是否存在,存在了即已加载过,就不需要重复加载了。
模块的内部方法是如何导出的
模块化开发的目的是为了避免污染全局变量,模块内部定义的方法,都是通过内部变量export的形式,提供外部访问句柄的。
1 | define('A', ['B','C'], function(require, exports) { |
上面代码表示模块A对外提供了一个hello()
方法, 在模块D内部通过require('A')
来取得模块A的内部对象exports,这样就可以使用hello方法啦。
我们先看下var A = require('A')
都做了些什么?
1 | function require(id) { |
利用call调用模块的factory方法,依次将require
,exports
对象传入,模块内部将hello方法挂在exports对象上,最后将exports对象挂在模块映射表上缓存起来,下次就可以直接使用了。
如何解决循环依赖
模块之间互相引用,会导致循环依赖:
1 | // a.js |
备注:这里的循环依赖与重复加载是不同的概念
重复加载指的是从网络下载模块文件,如何解决上面已经提到过了。
循环依赖的问题在于循环触发了模块的factory函数。解决的关键就是require('xxx')
时需要判断是否factory已经执行过了,如果执行了,直接返回上次的exports对象即可。
CMD,AMD区别
CMD推崇依赖就近,可以把依赖写进你的代码中的任意一行:
1 | define(function(require, exports, module) { |
代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。
AMD是依赖前置,也就是在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块:
1 | define(['./a','./b'],function(a,b){ |
由于提前知道依赖关系,所以不需要遍历整个函数体找它的依赖,所以性能相比CMD有所提升。唯一的缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大
当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。
硬依赖?软依赖
所谓硬依赖就是肯定要执行的依赖,但有时候偏偏有这样的情况:
1 | // 函数体内: |
我们把这种既可能依赖a,也可能不依赖a的这种叫做”软依赖”。为了最大化优化,我们需要针对这种情况做特殊处理:
1 | // 函数体内: |
这就是依赖前置+回调的方案。
参考资料
- 非 AMD 演示加载器地址 webkit-dwarf