JavaScript 模塊化編程(三):實現(xiàn)一個RequireJS

JavaScript 模塊化編程(一):模塊的寫法
JavaScript 模塊化編程(二):規(guī)范
JavaScript 模塊化編程(三):實現(xiàn)一個RequireJS
JavaScript 模塊化編程(四):結(jié)合Node源碼分析CommonJs規(guī)范


雖然RequireJS是一個過時的JS模塊化解決方案,但是其對我們了解JS模塊化的發(fā)展依然很重要。下面將參考RequireJS源碼寫了一個簡單的JavaScript模塊加載器。只有300行代碼,是源碼的六分之一,已經(jīng)放在我的github上

--------點擊查看代碼--------


1.RequireJS

先回顧一下RequireJS的兩個重要api

定義模塊
define(id?, dependencies?, factory)
加載模塊
require([module], callback)

require和define函數(shù)的關(guān)系
(1)require和define函數(shù)接收同樣的參數(shù),define和require在依賴處理和回調(diào)執(zhí)行上都是一樣的
(2)define的回調(diào)函數(shù)需要有return語句返回模塊對象,這樣define定義的模塊才能被其他模塊引用,所以用它來定義模塊(建議在一個文件中使用一次);require的回調(diào)函數(shù)不需要return語句。其實在我看來,require函數(shù)可以看做是特殊的define函數(shù),它用來定義一個頂層匿名模塊,用來加載和使用模塊,這個模塊不需要被其他模塊加載。

RequireJS中的執(zhí)行流程
(1)RequireJS首先找到data-main屬性,然后根據(jù)屬性值(通過新建一個script標(biāo)簽)加載并且解析入口文件
(2)先調(diào)用 require 函數(shù)和define函數(shù),將所有的依賴轉(zhuǎn)化為script 節(jié)點插入到dom 中,然后每一個 節(jié)點的onload事件中,將該模塊作為實體保存起來,并檢查所有模塊是否加載完成,如果加載完成,遞歸執(zhí)行所有回調(diào)


2.實現(xiàn)一個簡單的RequireJS

首先這里先說明一點,下面我們主要了解實現(xiàn)的整體流程,會粘出一些主要代碼,里面可能會包含一些輔助函數(shù),具體可以查看我github上的源碼

(1)定義一個全局變量
首先,需要定義幾個全局變量,用來保存已經(jīng)加載的模塊,尚未加載的模塊,所有模塊等全局信息

var  context={
      topModule:"", //頂層模塊
      waitings:[], // 尚未加載的模塊
      loadeds:[], // 已經(jīng)加載的模塊
      baseUrl:"",
        /**
         * 每一個模塊都有下面的幾個屬性:
         * moduleNmae  模塊名稱
         * deps 模塊依賴
         * factory 模塊工廠函數(shù) .
         * args 該模塊的依賴模塊的返回值
         * returnValue 該模塊工廠函數(shù)的返回值
         */
      modules:[] // 模塊集合
     } ;

(2)加載 require 頂層模塊
在require.js 里面都是用 data-main 屬性來指定入口文件,所以先尋找 data-main 屬性,并插入到 head中 。這里將 data-main 作為的路徑作為 baseUrl

/**
 * 查找data-main屬性的script標(biāo)簽,
 * 根據(jù)屬性值(通過新建一個script標(biāo)簽)加載并且解析入口文件
 */
if (isBrowser) {

     var scripts=document.getElementsByTagName('script');
     var head,src,subPath,mainScript;

     eachList(scripts,function(script){
      var dataMain = script.getAttribute('data-main');
            if (dataMain) {
                if (!head) {
                head = script.parentNode;
                }
                if (!context.baseUrl) {
                    src = dataMain.split('/');
                    mainScript = src.pop();
                    subPath = src.length ? src.join('/')  + '/' : './';
                    context.baseUrl = subPath;
                }
                // 創(chuàng)建頂層節(jié)點
                var dataMainNode = document.createElement('script');
                dataMainNode.async = true;
                head.appendChild(dataMainNode);
                dataMainNode.src = dataMain+ ".js";
                dataMainNode.onload = function() {
                    // 將頂層模塊 從waitings里面除去,并添加到loadeds數(shù)組中
                    removeByEle(context.waitings, context.topModule)
                    context.loadeds.push(context.topModule);
                }
                return true;
            }
     });

(3)定義 require 方法
require 方法用來使用模塊,也就是定義一個頂層模塊,這個模塊不需要被其他模塊加載

 /**
 * require方法,加載一個模塊
 * @param  {[type]}  deps    [依賴數(shù)組]
 * @param  {Function} callback [工廠函數(shù)]
 * @return {[type]}
 */
 requireJs.require=function(deps,callback){

        if (typeof name !== 'string') {
            callback = deps;
            deps = name;
            name = null;
        }

       if (!isArray(deps)) {
            callback = deps;
            deps = [];
        }

        // 生成隨機模塊名,方法
        let moduleName = getUnqName();
        context.topModule = moduleName;
        context.waitings.push(moduleName);
        // 生成一個模塊配置
        context.modules[moduleName] = {
            moduleName: moduleName,
            deps: deps,
            factory: callback,
            args: [],
            returnValue: ""
        }

        deps.forEach(function(dep) {
            var scriptNode = document.createElement("script");
            scriptNode.setAttribute("data-module-name", dep);
            scriptNode.async = true;
            scriptNode.src = context.baseUrl + dep + ".js";
            document.querySelector("head").appendChild(scriptNode);
            scriptNode.onload = scriptOnload;
            context.waitings.push(dep);
        });

 }

這里需要注意一個函數(shù)scriptOnload,在script 節(jié)點加載完成后觸發(fā)。將對應(yīng)模塊從waitings 里面刪除,同時往loadeds里面添加該模塊,如果發(fā)現(xiàn) waitings為空,那么就開始遞歸執(zhí)行工廠函數(shù) 。

/**
 *  [每一個腳本插入head中,都會執(zhí)行這個事件 。這個函數(shù)完成兩件事:
 *  1. 如果是一個匿名模塊加載,那么取得這個匿名模塊,并完成模塊命名,
 *  2. 當(dāng)節(jié)點加載完畢,判斷context.waitings是否為空,如果不為空,返回,如果為空,說明已經(jīng)全部加載完畢,現(xiàn)在就可以執(zhí)行所有的工廠函數(shù)]
 * @param  {[object]} e [事件對象]
 * @return {[type]}
 */
function scriptOnload(e) {
    e = e || window.event;
    let node = e.target;
    let moduleName = node.getAttribute('data-module-name');
    tempModule.moduleName = moduleName;
    context.modules[moduleName] = tempModule;
    removeByEle(context.waitings, moduleName);
    context.loadeds.push(moduleName);

    if (!context.waitings.length) {
        console.log(context.modules);
        exec(context.topModule);
    }
}

(4)定義 define 方法
其實define函數(shù)和上面的require函數(shù)做了差不多相同的事,差別在于require自動生成了一個模塊名。并且require中設(shè)置了context.topModule

/**
 * [define和 require 做的工作幾乎相同]
 * @param  {[array]} deps    [依賴數(shù)組]
 * @param  {[function]} callback [工廠函數(shù)]
 * @return {[type]}
 */
 requireJs.define=function(name,deps,callback){

      if (typeof name !== 'string') {
            callback = deps;
            deps = name;
            name = null;
        }

      if (!isArray(deps)) {
            callback = deps;
            deps = [];
        }

        //生成一個模塊配置
        tempModule = {
            deps: deps,
            factory: callback,
            args: [],
            returnValue: ""
        }

        // 遞歸遍歷所有依賴,添加到 `head` 中,并設(shè)置 這個節(jié)點的一個屬性`data-module-name`標(biāo)識模塊名
        deps.forEach(function(dep) {
            var scriptNode = document.createElement("script");
            scriptNode.setAttribute("data-module-name", dep);
            scriptNode.async = true;
            scriptNode.src = context.baseUrl + dep + ".js";
            document.querySelector("head").appendChild(scriptNode);
            scriptNode.onload = scriptOnload;
            context.waitings.push(dep);
        });
 }

(5)執(zhí)行回調(diào)
我們再回到scriptOnload 函數(shù),每個模塊加載完成,就會在 waitings 里面去掉,然后檢查waitings 數(shù)組,如果為空,說明全部加載完,就可以執(zhí)行 exec函數(shù),在這里函數(shù)中,遞歸執(zhí)行所有的回調(diào) 。

/**
 * 所有模塊加載完畢,遞歸執(zhí)行工程函數(shù) , 核心方法
 * @param  {[string]} moduleName [模塊名]
 * @return {[type]}
 */
function exec(moduleName) {
    let module = context.modules[moduleName];
    let deps = module.deps;
    let args = [];
    if(deps){
        deps.forEach(function(dep) {
            exec(dep);
            args.push(context.modules[dep].returnValue);
        });
        module.args = args;
        module.returnValue = context.modules[moduleName].factory.apply(context.modules[moduleName], args);
    }
}

3.測試
//user.js
define([], function () {
    return {
        checkLogin: function (name,pwd) { 
            return name==="xxx"&&pwd==="yyy"
        }
    }
})

//math.js
define(function () {
    return {
        add: function (a,b){
            return a+b;
        },
        sub:function(a,b){
            return a-b;
        }     
    }
})
//main.js
require(["math","user"], function(math,user) {
  if(user.checkLogin("xxx","yyz")){
    console.log("12+21=" + math.add(12,21));
  }else{
    console.log('please sign in or register first');
  }
})
//test.html
 <script src="../require.js" data-main="main"></script>

結(jié)果

result.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容