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


示例代碼托管在我的代碼倉:http://www.github.com/dashnowords/blogs

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

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

一. 概述

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

二. require.js

2.1 基本用法

require.js是一個實現了AMD(不清楚AMD規范的同學請戳這里【AMD模塊化規范】)模塊管理規范的庫(require.js同時也能夠識別CMD規范的寫法),基本的使用方法也非常簡單:

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

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

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

  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函數定義

    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倉庫中獲取,歡迎點星星。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com