JavaScript 模块化及 SeaJs 源码分析

开发 前端
和后端(比如Java)比较就可以看出明显的差距。2009年Ryan Dahl创建了node.js项目,将JavaScript用于服务器编程,这标志“JS模块化编程”正式诞生。

网页的结构越来越复杂,简直可以看做一个简单APP,如果还像以前那样把所有的代码都放到一个文件里面会有一些问题:

  • 全局变量互相影响

  • JavaScript文件变大,影响加载速度

  • 结构混乱、很难维护

和后端(比如Java)比较就可以看出明显的差距。2009年Ryan Dahl创建了node.js项目,将JavaScript用于服务器编程,这标志“JS模块化编程”正式诞生。

基本原理

模块就是一些功能的集合,那么可以将一个大文件分割成一些小文件,在各个文件中定义不同的功能,然后在HTML中引入:

  1. var module1 = new Object({ 
  2.     _count : 0
  3.     m1 : function (){ 
  4.         //... 
  5.     }, 
  6.     m2 : function (){ 
  7.         //... 
  8.     } 
  9. }); 

这样做的坏处是:把模块中所有的成员都暴露了!我们知道函数的本地变量是没法从外面进行访问的,那么可以用立即执行函数来优化:

  1. var module1 = (function(){ 
  2.     var _count = 0
  3.     var m1 = function(){ 
  4.         //... 
  5.     }; 
  6.     var m2 = function(){ 
  7.         //... 
  8.     }; 
  9.     return { 
  10.         m1 : m1, m2 : m2 
  11.     }; 
  12. })(); 

大家定义模块的方式可能五花八门,如果都能按照一定的规范来,那好处会非常大:可以互相引用!

模块规范

在node.js中定义math.js模块如下:

  1. function add(a, b){ 
  2.     return a + b; 
  3. exports.add = add; 

在其他模块中使用的时候使用全局require函数加载即可:

  1. var math = require('math'); 
  2. math.add(2,3); 

在服务器上同步require是没有问题的,但是浏览器在网络环境就不能这么玩了,于是有了异步的AMD规范

  1. require(['math'], function (math) {// require([module], callback); 
  2.     math.add(23); 
  3. }); 

模块的定义方式如下(模块可以依赖其他的模块):

  1. define(function (){ // define([module], callback); 
  2.     var add = function (x,y){ 
  3.         return x+y; 
  4.     }; 
  5.     return { add: add }; 
  6. }); 

用RequireJS可以加载很多其他资源(看这里),很好很强大!在工作中用的比较多的是SeaJS,所使用的规范称为CMD,推崇(应该是指异步模式):

as lazy as possible!

对于依赖的模块的处理方式和AMD的区别在于:

AMD是提前执行(依赖前置),CMD是延迟执行(依赖就近)。

在CMD中定义模块的方式如下:

  1. define(function(require, exports, module) { 
  2.     var a = require('./a'); 
  3.     a.doSomething(); 
  4.     var b = require('./b'); 
  5.     b.doSomething(); 
  6. }); 

使用方式直接看文档,这里就不赘述了!

SeaJS源码分析

刚接触模块化的时候感觉这个太简单了,不就是:

创建script标签的时候设置一下onload和src!

事实上是这样的,但也不完全是!下面来开始看SeaJS的代码(sea-debug.js)。一个模块在加载的过程中可能经历下面几种状态:

  1. var STATUS = Module.STATUS = { 
  2.     // 1 - The `module.uri` is being fetched 
  3.     FETCHING: 1
  4.     // 2 - The meta data has been saved to cachedMods 
  5.     SAVED: 2
  6.     // 3 - The `module.dependencies` are being loaded 
  7.     LOADING: 3
  8.     // 4 - The module are ready to execute 
  9.     LOADED: 4
  10.     // 5 - The module is being executed 
  11.     EXECUTING: 5
  12.     // 6 - The `module.exports` is available 
  13.     EXECUTED: 6
  14.     // 7 - 404 
  15.     ERROR: 7 

内存中用Modul对象来维护模块的信息:

  1. function Module(uri, deps) { 
  2.     this.uri = uri 
  3.     this.dependencies = deps || [] // 依赖模块ID列表 
  4.     this.deps = {} // 依赖模块Module对象列表 
  5.     this.status = 0 // 状态 
  6.     this._entry = [] // 在模块加载完成之后需要调用callback的模块 

在页面上启动模块系统需要使用seajs.use方法:

  1. seajs.use(‘./main’, function(main) {// 依赖及回调方法 
  2.     main.init(); 
  3. }); 

加载过程的整体逻辑可以在Module.prototype.load中看到:

  1. Module.prototype.load = function() { 
  2.     var mod = this 
  3.     if (mod.status >= STATUS.LOADING) { 
  4.         return 
  5.     } 
  6.     mod.status = STATUS.LOADING 
  7.     var uris = mod.resolve() // 解析依赖模块的URL地址 
  8.     emit("load", uris) 
  9.     for (var i = 0, len = uris.length; i < len; i++) { 
  10.         mod.deps[mod.dependencies[i]] = Module.get(uris[i])// 从缓存取或创建 
  11.     } 
  12.     mod.pass(); // 将entry传递给依赖的但还没加载的模块 
  13.     if (mod._entry.length) {// 本模块加载完成 
  14.         mod.onload() 
  15.         return 
  16.     } 
  17.     var requestCache = {}; 
  18.     var m; 
  19.     // 加载依赖的模块 
  20.     for (i = 0; i < len; i++) { 
  21.         m = cachedMods[uris[i]] 
  22.         if (m.status < STATUS.FETCHING) { 
  23.             m.fetch(requestCache) 
  24.         } else if (m.status === STATUS.SAVED) { 
  25.             m.load() 
  26.         } 
  27.     } 
  28.     for (var requestUri in requestCache) { 
  29.         if (requestCache.hasOwnProperty(requestUri)) { 
  30.             requestCache[requestUri]() 
  31.         } 
  32.     } 

总体上逻辑很顺就不讲了,唯一比较绕的就是_entry数组了。网上没有找到比较通俗易懂的文章,于是看着代码连蒙带猜地大概看懂了,其实只要记住它的目标即可:

当依赖的所有模块加载完成后执行回调函数!

换种说法:

数组_entry中保存了当前模块加载完成之后、哪些模块的依赖可能加载完成的列表(依赖的反向关系)!

举个例子,模块A依赖于模块B、C、D,那么经过pass之后的状态如下:

此时A中的remain为3,也就是说它还有三个依赖的模块没有加载完成!而如果模块B依赖模块E、F,那么在它load的时候会将A也传递出去:

有几个细节:

  1. 已经加载完成的模块不会被传播;

  2. 已经传播过一次的模块不会再次传播;

  3. 如果依赖的模块正在加载那么会递归传播;

维护好依赖关系之后就可以通过Module.prototype.fetch来加载模块,有两种sendRequest的实现方式:

  1. importScripts

  2. script

然后根据结果执行load或者error方法。依赖的所有模块都加载完成后就会执行onload方法:

  1. Module.prototype.onload = function() { 
  2.     var mod = this 
  3.     mod.status = STATUS.LOADED 
  4.     for (var i = 0, len = (mod._entry || []).length; i < len; i++) { 
  5.         var entry = mod._entry[i] 
  6.         if (--entry.remain === 0) { 
  7.             entry.callback() 
  8.         } 
  9.     } 
  10.     delete mod._entry 

其中--entry.remain就相当于告诉entry对应的模块:你的依赖列表里面已经有一个完成了!而entry.remain === 0则说明它所依赖的所有的模块都已经加载完成了!那么此时将执行回调函数:

  1. for (var i = 0, len = uris.length; i < len; i++) { 
  2.     exports[i] = cachedMods[uris[i]].exec(); 
  3. if (callback) { 
  4.     callback.apply(global, exports)// 执行回调函数 

脚本下载完成之后会马上执行define方法来维护模块的信息:

没有显式地指定dependencies时会用parseDependencies来用正则匹配方法中的require()片段(指定依赖列表是个好习惯)。

接着执行factory方法来生成模块的数据:

  1. var exports = isFunction(factory) ? 
  2.     factory.call(mod.exports = {}, require, mod.exports, mod) : 
  3.     factory 

然后执行你在seajs.use中定义的callback方法:

  1. if (callback) { 
  2.     callback.apply(global, exports) 

当你写的模块代码中require时,每次都会执行factory方法:

  1. function require(id) { 
  2.     var m = mod.deps[id] || Module.get(require.resolve(id)) 
  3.     if (m.status == STATUS.ERROR) { 
  4.         throw new Error('module was broken: ' + m.uri) 
  5.     } 
  6.     return m.exec() 

到这里核心的逻辑基本上讲完了,补一张状态的转换图:

以后在用的时候就可以解释一些诡异的问题了!

总结

模块化非常好用,因此在ECMAScript 6中也开始支持,但是浏览器支持还是比较堪忧的~~

责任编辑:王雪燕 来源: WsztRush
相关推荐

2019-08-28 16:18:39

JavaScriptJS前端

2017-04-10 14:23:01

typescriptjavascriptwebpack

2013-08-20 18:39:34

JavaScript模requireJS

2010-05-21 18:26:58

2020-10-09 06:40:53

恶意软件

2020-05-12 08:39:50

JavaScript工具技术

2020-09-17 10:30:21

前端模块化组件

2013-08-20 15:31:18

前端模块化

2017-05-18 10:23:55

模块化开发RequireJsJavascript

2020-09-18 09:02:32

前端模块化

2015-10-10 11:29:45

Java模块化系统初探

2022-03-11 13:01:27

前端模块

2010-01-12 16:15:02

模块化交换机

2018-03-21 21:31:28

Java9编程Java

2022-09-05 09:01:13

前端模块化

2018-06-21 09:36:09

模块化数据中心集中化

2019-09-02 10:51:59

Python脚本语言程序员

2016-10-09 11:03:41

Javascript模块化Web

2017-05-18 11:43:41

Android模块化软件

2016-12-21 17:02:35

数据中心MDC模块化
点赞
收藏

51CTO技术栈公众号