100行以内实现基于观察者模式的JS模块加载器

255 阅读3分钟

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和关注哈,以后还会给大家写更多的轮子~