javascript基礎修煉(12)——手把手教你造一個簡易的require.js

示例代碼託管在個人代碼倉:http://www.github.com/dashnowords/blogshtml

博客園地址:《大史住在大前端》原創博文目錄前端

華爲雲社區地址:【你要的前端打怪升級指南】java

一. 概述

許多前端工程師沉浸在使用腳手架工具的快感中,認爲require.js這種前端模塊化的庫已通過氣了,的確若是隻從使用場景來看,在以webpack爲首的自動化打包趨勢下,大部分的新代碼都已經使用CommonJsES Harmony規範實現前端模塊化,require.js的確看起來沒什麼用武之地。可是前端模塊化的基本原理卻基本都是一致的,不管是實現了模塊化加載的第三方庫源碼,仍是打包工具生成的代碼中,你均可以看到相似的模塊管理和加載框架,因此研究require.js的原理對於前端工程師來講幾乎是不可避免的,即便你繞過了require.js,也會在後續學習webpack的打包結果時學習相似的代碼。研究模塊化加載邏輯對於開發者理解javascript回調的運行機制很是有幫助,同時也能夠提升抽象編程能力。webpack

二. require.js

2.1 基本用法

require.js是一個實現了AMD(不清楚AMD規範的同窗請戳這裏【AMD模塊化規範】)模塊管理規範的庫(require.js同時也可以識別CMD規範的寫法),基本的使用方法也很是簡單:git

  1. 類庫引入,在主頁index.html中引入require.js:github

    <script src="require.js" data-main="main.js"></script>

    data-main自定義屬性指定了require.js完成初始化後應該加載執行的第一個文件。web

  2. main.js中調用require.config傳入配置參數,並經過require方法傳入主啓動函數:編程

    //main.js
    require.config((
        baseUrl:'.',
        paths:{
           jQuery:'lib/jQuery.min',
           business1:'scripts/business1',
           business2:'scripts/business2',
           business3:'scripts/business3'
        }
    ))
    
    require(['business1','business2'],function(bus1,bus2){
         console.log('主函數執行');
         bus2.welcome();
    });
  3. 模塊定義經過define函數定義json

    define(id?:string, deps?:Array<string>, factory:function):any
  4. 訪問index.html後的模塊加載順序:

    訪問的順序從require方法執行開始打亂,main.js中的require方法調用聲明瞭對business1business2兩個模塊的依賴,那麼最後一個參數(主方法)不會當即解析,而是等待依賴模塊加載,當下載到定義business1模塊的文件scripts/business1.js後,寫在該文件中的define方法會被執行,此時又發現當前模塊依賴business3模塊,程序又會延遲生成business1模塊的工廠方法(也就是scripts/business1.js中傳入define方法的最後一個函數參數),轉而先去加載business3這個模塊,若是define方法沒有聲明依賴,或者聲明的依賴都已經加載,就會執行傳入的工廠方法生成指定模塊,不難理解模塊的解析是從葉節點開始最終在根節點也就是主工廠函數結束的。

    因此模塊文件加載順序和工廠方法執行順序基本是相反的,最早加載的模塊文件中的工廠方法可能最後才被運行(也多是亂序,但符合依賴關係),由於須要等待它依賴的模塊先加載完成,運行順序可參考下圖(運行結果來自第三節中的demo):

2.2 細說API設計

require.js在設計上貫徹了多態原則,API很是精簡。

模塊定義的方法只有一個define,可是包含了很是多狀況:

  • 1個參數

    • function類型

      將參數斷定爲匿名模塊的工廠方法,僅起到做用域隔離的做用。

    • object類型

      將模塊識別爲數據模塊,可被其餘模塊引用。

  • 2個參數

    • string+function | object

      第一參數做爲模塊名,第二參數做爲模塊的工廠方法或數據集。

    • array<string>+function | object

      第一參數做爲依賴列表,第二參數做爲匿名模塊工廠方法或數據集。

  • 3個參數

    第一個參數做爲模塊名,第二個參數做爲依賴列表,第三個參數做爲工廠方法或數據集。

  • deps : array<string>依賴列表中成員的解析

    • 包含/./../

      斷定爲依賴資源的地址

    • 不包含上述字符

      斷定爲依賴模塊名

模塊加載方法require也是諸多方法的集合:

  • 1個參數

    • string類型

      按照模塊名或地址來加載模塊。

    • array類型

      當作一組模塊名或地址來加載,無加載後回調。

  • 2個參數

    第一個參數做爲依賴數組,第二個參數做爲工廠方法。

在這樣的設計中,不一樣參數類型對應的函數重載在require.js內部進行斷定分發,使得由用戶編寫的調用邏輯顯得更加簡潔一致。

三. 造輪子

做爲前端工程師,只學會使用方法是遠遠不夠的,本節中咱們使用「造輪子」的方法造一個簡易的require.js,以便探究其中的原理。本節使用的示例中,先加載require.js,入口文件爲main.js,主邏輯中前置依賴爲business1business2兩個模塊,business1依賴於business3模塊,business2依賴於jQuery。以下所示:

3.1 模塊加載執行的步驟

上一節在分析require.js執行步驟時咱們已經看到,當一個模塊依賴於其餘模塊時,它的工廠方法(requiredefine的最後一個參數)是須要先緩存起來的,程序須要等待依賴模塊都加載完成後纔會執行這個工廠方法。須要注意的是,工廠方法的執行順序只能從依賴樹的葉節點開始,也就是說咱們須要一個棧結構來限制它的執行順序,每次先檢測棧頂模塊的依賴是否所有下載解析完畢,若是是,則執行出棧操做並執行這個工廠方法,而後再檢測新的棧頂元素是否知足條件,以此類推。

define方法的邏輯是很是相似的,如今moduleCache中登記一個新模塊,若是沒有依賴項,則直接執行工廠函數,若是有依賴項,則將工廠函數推入unResolvedStack待解析棧,而後依次對聲明的依賴項調用require方法進行加載。

咱們會在每個依賴的文件解析完畢觸發onload事件時將對應模塊的緩存信息中的load屬性設置爲true,而後執行檢測方法,來檢測unResolvedStack的棧頂元素的依賴項是否都已經都已經完成解析(解析完畢的依賴項在moduleCache中記錄的對應模塊的load屬性爲true),若是是則執行出棧操做並執行這個工廠方法,而後再次運行檢測方法,直到棧頂元素當前沒法解析或棧爲空。

3.2 代碼框架

咱們使用基本的閉包自執行函數的代碼結構來編寫requireX.js(示例中只實現基本功能):

;(function(window, undefined){
    //模塊路徑記錄
    let modulePaths = {
        main:document.scripts[0].dataset.main.slice(0,-3) //data-main傳入的路徑做爲跟模塊
    };
    //模塊加載緩存記錄
    let moduleCache = {};
    //待解析的工廠函數
    let unResolvedStack = [];
    //匿名模塊自增id
    let anonymousIndex = 0;
    //空函數
    let NullFunc =()=>{};
    
    /*moduleCache中記錄的模塊信息定義*/
    class Module {
        constructor(name, path, deps=[],factory){
            this.name = name;//模塊名
            this.deps = deps;//模塊依賴
            this.path = path;//模塊路徑
            this.load = false;//是否已加載
            this.exports = {};//工廠函數返回內容
            this.factory = factory || NullFunc;//工廠函數
        }
    }
    
    //模塊加載方法
    function _require(...rest){
        //...
    }
    
    //模塊定義方法
    function _define(...rest){
        
    }
    
    //初始化配置方法
    _require.config = function(conf = {}){
        
    }
    
    /**
    *一些其餘的內部使用的方法
    */
    
    //全局掛載
    window.require = _require;
    window.define = _define;
    
    //從data-main指向開始解析
    _require('main');
    
})(window);

3.3 關鍵函數的代碼實現

下面註釋覆蓋率超過90%了,不須要再多說什麼。

  1. 加載方法_require(省略了許多條件判斷,只保留了核心邏輯)
function _require(...rest){
        let paramsNum = rest.length;
        switch (paramsNum){
            case 1://若是隻有一個字符串參數,則按模塊名對待,若是隻有一個函數模塊,則直接執行
                if (typeof rest[0] === 'string') {
                    return _checkModulePath(rest[0]);
                }
            break;
            case 2:
                if (Object.prototype.toString.call(rest[0]).slice(8,13) === 'Array' && typeof rest[1] === 'function'){
                    //若是依賴爲空,則直接運行工廠函數,並傳入默認參數
                    return _define('anonymous' + anonymousIndex++, rest[0], rest[1]);
                }else{
                    throw new Error('參數類型不正確,require函數簽名爲(deps:Array<string>, factory:Function):void');
                }
            break;
        }
    }

若是傳入一個字符,則將其做爲模塊名傳入_checkModulePath方法檢測是否有註冊路徑,若是有路徑則去獲取定義這個模塊的文件,若是傳入兩個參數,則運行_define方法將其做爲匿名模塊的依賴和工廠函數處理。

  1. 模塊定義方法_define
function _define(id, deps, factory){
        let modulePath = modulePaths[id];//獲取模塊路徑,多是undefined
        let module = new Module(id, modulePath, deps, factory);//註冊一個未加載的新模塊
        moduleCache[id] = module;//模塊實例掛載至緩存列表
        _setUnResolved(id, deps, factory);//處理模塊工廠方法延遲執行邏輯
    }
  1. 延遲執行工廠方法的函數_setUnResolved
function _setUnResolved(id, deps, factory) {
        //壓棧操做緩存要延遲執行的工廠函數
        unResolvedStack.unshift({id, deps,factory});
        //遍歷依賴項數組對每一個依賴執行檢測路徑操做,檢測路徑存在後對應的是js文件獲取邏輯
        deps.map(dep=>_checkModulePath(dep));
    }
  1. 模塊加載邏輯_loadModule
function _loadModule(name, path) {
        //若是存在模塊的緩存,表示已經登記,不須要再次獲取,在其onload回調中修改標記後便可被使用
        if(name !== 'root' && moduleCache[name]) return;
        //若是沒有緩存則使用jsonp的方式進行首次加載
        let script = document.createElement('script');
            script.src = path + '.js';
            script.defer = true;
            //初始化待加載模塊緩存
            moduleCache[name] = new Module(name,path);
            //加載完畢後回調函數
            script.onload = function(){
                //修改已登記模塊的加載解析標記
                moduleCache[name].load = true;
                //檢查待解析模塊棧頂元素是否可解析
                _checkunResolvedStack();
            }
            console.log(`開始加載${name}模塊的定義文件,地址爲${path}.js`);
            //開始執行腳本獲取
            document.body.appendChild(script);
    }
  1. 檢測待解析工廠函數的方法_checkunResolvedStack
function _checkunResolvedStack(){
        //若是沒有待解析模塊,則直接返回
        if (!unResolvedStack.length)return;
        //不然查看棧頂元素的依賴是否已經所有加載
        let module = unResolvedStack[0];
        //獲取聲明的依賴數量
        let depsNum = module.deps.length;
        //獲取已加載的依賴數量
        let loadedDepsNum = module.deps.filter(item=>moduleCache[item].load).length;
        //若是依賴已經所有解析完畢
        if (loadedDepsNum === depsNum) {
            //獲取全部依賴的exports輸出
            let params = module.deps.map(dep=>moduleCache[dep].exports);
            //運行待解析模塊的工廠函數並掛載至解析模塊的exports輸出
            moduleCache[module.id].exports = module.factory.apply(null,params);
            //待解析模塊出棧
            unResolvedStack.shift();
            //遞歸檢查
            return _checkunResolvedStack();
        }
    }

示例的效果是頁面中提示語緩慢顯示出來。的完整的示例代碼可從篇頭的github倉庫中獲取,歡迎點星星。

相關文章
相關標籤/搜索