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; }; }]);