视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
详细介绍JavaScript模块化及SeaJs源码分析
2020-11-27 20:24:18 责编:小采
文档

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

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

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

define(function(require, exports, module) {
 var a = require('./a');
 a.doSomething();
 var b = require('./b');
 b.doSomething();
});

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

SeaJS源码分析

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

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

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

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

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

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

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

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

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

Module.prototype.load = function() {
 var mod = this
 if (mod.status >= STATUS.LOADING) {
 return
 }
 mod.status = STATUS.LOADING
 var uris = mod.resolve() // 解析依赖模块的URL地址
 emit("load", uris)
 for (var i = 0, len = uris.length; i < len; i++) {
 mod.deps[mod.dependencies[i]] = Module.get(uris[i])// 从缓存取或创建
 }
 mod.pass(); // 将entry传递给依赖的但还没加载的模块
 if (mod._entry.length) {// 本模块加载完成
 mod.onload()
 return
 }
 var requestCache = {};
 var m;
 // 加载依赖的模块
 for (i = 0; i < len; i++) {
 m = cachedMods[uris[i]]
 if (m.status < STATUS.FETCHING) {
 m.fetch(requestCache)
 } else if (m.status === STATUS.SAVED) {
 m.load()
 }
 }
 for (var requestUri in requestCache) {
 if (requestCache.hasOwnProperty(requestUri)) {
 requestCache[requestUri]()
 }
 }
}

总体上逻辑很顺就不讲了,唯一比较绕的就是_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方法:

Module.prototype.onload = function() {
 var mod = this
 mod.status = STATUS.LOADED
 for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
 var entry = mod._entry[i]
 if (--entry.remain === 0) {
 entry.callback()
 }
 }
 delete mod._entry
}

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

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

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

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

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

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

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

if (callback) {
 callback.apply(global, exports)
}

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

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

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

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

总结

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

下载本文
显示全文
专题