JavaScript语言官方未实现命名空间,我们定义一个define函数以实现命名空间。define函数的使用如:
define(function(exports, module, require) {
const $ = require('http://path/to/defined-jquery');
$(function(){
// dom ready!!!
});
});
我们可以这样实现:
(function(global) {
'use strict';
var errMsg = Math.random().toString(32).substr(2);
// rootModule 对象保存着所有已加载的模块
var rootModule = {};
// 每一个模块实例都有id属性作为require时查找的标识符,
// exports属性作为对外暴露的对象,loaded属性表示是否加载完。
function ModuleCtor(id) {
if (!this || this.__proto__ !== ModuleCtor.prototype) {
return new ModuleCtor(id);
}
this.id = id;
this.exports = {};
this.loaded = !1;
}
function define(id, fn) {
// 手动赋值模块id,兼容一个script里有多个define。
if (typeof id === 'function') {
fn = id;
id = document.currentScript
? document.currentScript.src
: Math.random()
.toString(32)
.substr(2);
}
if (typeof id !== 'string') {
id = '' + id;
}
var module = ModuleCtor(id);
exec();
function __require__(src) {
// 如果依赖已经加载过直接返回module.exports,
// 如果没有加载过则通过jsonp加载,并且抛出一个异常来打断原函数执行,在子模块加载完毕后重新执行原函数模拟异步代码阻塞同步执行。
if (rootModule[src] && rootModule[src].__proto__ === ModuleCtor.prototype) {
return rootModule[src].exports;
}
loadScript(src, function() {
exec();
});
throw new Error(errMsg);
}
function exec() {
// 将__require__函数传入fn,来支持模块内引用其他模块。
try {
fn.call(module.exports, module.exports, module, __require__);
module.loaded = !0;
rootModule[id] = module;
} catch (err) {
if (err.message !== errMsg) {
throw err;
}
}
}
}
function loadScript(src, callback) {
var script = document.createElement('script');
script.src = src;
script.onload = function() {
callback && callback(src);
};
document.body.appendChild(script);
return script;
}
// 暴露define给全局
global.define = define;
})(window);
这个模块加载的实现有很多不足,如果模块内有很多require时会被执行很多次,所以最好子模块内都是函数不要有自己的状态。seajs的依赖解决方法是,调用函数的toString方法来获得函数字面量,然后在parse出模块依赖,先加载依赖,每一个依赖加载完成后都emit一个事件,当所有依赖都加载完毕后,才执行factory函数,factory函数只执行一次,但是模块加载的顺序和require的顺序基本没有关系(并发请求,谁都有可能先到)。
======= 一本正经的分割线 ======
顺便吐槽一下seajs,由于某种原因,我再8102年见到了seajs而我在3000年前没用过。文档始终没有交代require('caonima');是如何打断函数执行的。看了源码发现是用了Function.prototype.toString方法,然后分析依赖并发加载(require函数是没有顺序之分的)。看源码前,我自己为了模拟该行为,通过抛出异常再反复的重新执行也实现了一个文件加载,而且我这个更贱的货还是真的同步引入依赖,更加cmd一些。
附 Webpack 模块加载原理:
(function(modulesArr) {
var rootModule = {};
function __require__(id) {
if (rootModule[id]) {
return rootModule[id].exports;
}
var currentModule = modulesArr[id];
var module = {
id,
exports: {}
};
currentModule.call(module.exports, module.exports, module, __require__);
currentModule[id] = module;
return module.exports;
}
return __require__(0);
})([
function(exports, module, require) {
var m1 = require(1);
console.log(m1);
},
function(exports, module, require) {
exports.msg = 'Hello World';
var m2 = require(2);
m2();
},
function(exports, module, require) {
module.exports = function() {
var str = 'Hello World';
console.log(str);
return str;
};
}
]);