0x00 前言
前端模块化至今仍然是离不开的话题,现今前端模块化存在CMD/AMD/CommonJS/ES6等几种规范和对应的实现方案。随着前端技术的发展特别是webpack和ES6语言自带的模块化出现以后,requireJS显得有点过时了。 然而自己写一个类似requireJS的轮子仍然是对基本功的一个很好的锤炼,因此这篇文章主要讲述如何写一个requireJS-like的模块加载器。RequireJS的实现是基于onload和轮询的,而我使用基于订阅发布的设计模式,算是一种不同的角度吧。
0x01 实现思路
- 一个require函数执行的时候,记录这个函数的内容作为一个特殊的模块。
- 如果函数没有任何依赖,则直接执行函数返回结构;否则,检测其依赖的模块的状态。
- 如果模块信息还不存在,初始化模块化信息;如果该模块只是初始化,那么可以开始下载这个模块,并订阅这个模块;如果该模块还在Loading,订阅这个模块,这样该模块下载完的时候就能收到通知;如果该模块已经Loaded但是还没执行,那么订阅这个模块,并让该模块去进行检测自己的状态以便加载依赖的模块和执行callback。
0x12 具体实现
一个模块的信息包括:
name(模块名
state(模块的状态)
deps(该模块依赖的其他模块)
context(代码块,即requirejs的callback)
contenxt(context执行后的模块内容)
deptas(哪个模块依赖于这个模块,用于发布加载完成的信息)
模块的状态分为:
NULL(已经初始化)
LOADING(正在下载)
LOADED(已经知道了具体的deps和逻辑代码但是没执行)
DONE(模块已经执行,可以使用)
模块信息字典
var modules = {};
模块信息构造器
function Module(name = '', deps = [], state = STATE.NULL, context = null, content = null, deptas = []) {
this.name = name;
this.deps = deps;
this.state = state;
this.context = context;
this.content = content;
this.deptas = deptas;
};
下载脚本
function loadJS(url, cb) {
var node = document.createElement('script');
node.async = true;
node.type = 'text/javascript';
node.onload = function () {
if (_.isFunction(cb)) {
cb();
}
};
node.error = function () {
throw Error('load script:' + url + 'fail');
};
node.src = url;
var head = document.getElementsByTagName('head')[0];
head.appendChild(node);
};
检查模块是否可以执行
/**
* check if deps are all ready
*/
Module.prototype.checkDeps = function () {
return _.isEmptyArray(this.deps) ?
true
:
_.every(this.deps, function (depName) {
return depName in modules && modules[depName].state === STATE.DONE;
});
};
观察者相关的模块
/**
* subscribe modules as to get notifications when it is done
* @param {String} depName the modules need to subscribe
*/
Module.prototype.sub = function (deptaName) {
if(modules[deptaName].deptas.indexOf(this.name) === -1){
modules[deptaName].deptas.push(this.name);
}
};
/**
* propogate the news of this moudle has been ready to the deptas
*/
Module.prototype.notify = function () {
_.forEach(this.deptas, function (name) {
if(modules[name].isReady()){
modules[name].complete();
}
});
};
require函数逻辑
/**
* require deps, start loading deps
*/
Module.prototype.require = function () {
// just run if ready
if(this.isReady()){
this.complete();
}
else {
var self = this;
_.forEach(this.deps, function (depName) {
let depModule = modules[depName];
if (depModule) {
switch (depModule.state) {
// subscribe if loading
case STATE.LOADING:
self.sub(depName);
break;
// the module context has been loaded but its deps might not, sub & require
case STATE.LOADED:
self.sub(depName);
depModule.require();
break;
// the module context remains unknow, sub & start loading the script
case STATE.NULL:
depModule.state = LOADING;
self.sub(depName);
loadModule(depName);
break;
default:
break;
}
}
else {
// the module has not been initialized yet
modules[depName] = new Module(depName, [], STATE.LOADING, null, null);
self.sub(depName);
loadModule(depName);
}
});
}
};
define逻辑
/**
* definiation a module
* @param {String} moduleName
* @param {Arary} deps
* @param {Function} callback context of the module
*/
var define = function (moduleName, deps, callback) {
var mod = modules[moduleName];
if(mod){
// some tasks have asked for this module, but until now the deps are known, start loading scripts of the deps
mod.deps = deps;
mod.context = callback;
mod.require();
}
else{
// lazy loading since no tasks have asked for this module yet, no need to load it.
modules[moduleName] = new Module(moduleName,deps,STATE.LOADED,callback,null);
}
};
0x03 Pros & Cons
Pros
- 核心代码100行不到
- 比起requireJS的实现思路,pub/sub的模式很容易理解
- 启用了async关键字,并行下载
- lazy loading, 只define但是没有被依赖的模块不会执行
Cons
- 只能跑在浏览器
- 性能可能是个问题,可用图算法优化
0x04 其他
项目github地址 github.com/XinScript 欢迎大家给star和关注哈,以后还会给大家写更多的轮子~