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

来源:https://www.cnblogs.com/dashnowords/archive/2019/05/05/10816039.html
-Advertisement-
Play Games

示例代碼托管在我的代碼倉: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. 概述 許多前端工程師沉浸在使用腳手架工具的快感中,認為 這種前端模塊化 ...


目錄

示例代碼托管在我的代碼倉: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倉庫中獲取,歡迎點星星。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 什麼是解構賦值 解構賦值允許你使用類似數組或對象字面量的語法將數組和對象的屬性賦給各種變數。這種賦值語法極度簡潔,同時還比傳統的屬性訪問方法更為清晰。解構會僅提取出一個大對象中的部分成員來單獨使用。 如下是傳統的: 但在ES6中可以簡寫為: 2 數組與對象的解構 數組解構賦值語法的一般形式為: ...
  • 那麼如何實現裝飾器的使用呢? 1.在命令行工具中使用 npm run eject。不熟的情況下可能會報錯,如果報錯的信息大概意思是:有些文件未被追蹤到,那麼直接git add . 再 git commit -m "",或者直接在.gitignore中忽略這些文件(不建議)2.npm run ejec ...
  • jsonp的本質是通過script標簽的src屬性請求到服務端,拿到到服務端返回的數據 ,因為src是可以跨域的。前端通過src發送跨域請求時在請求的url帶上回調函數,服務端收到請求時,接受前端傳過來的回掉函數名稱,將其拼接成js函數調用返回到前端即可完成跨域請求。 前端實現代碼: .Net服務端 ...
  • 前言 今天繼續typescript的學習,開始ts類的學習。 類 類的實現 在ES6中新增了類的概念。我們先看ES6中類的實現。 在ts中類的定義。ts中類的定義跟ES6差別不大。只是ts中新增了對屬性的類型的校驗。因此我們在使用屬性之前必須先定義屬性的類型。比如下麵Person類中的name和ag ...
  • H5-表單筆記 結果: ...
  • 複習 如何區別 JS DOM對象和 JQ 包裝對象? JQ對象其實是經過包裝的DOM對象,包裝後可調用 JQ 的方法。 JS 對象基本上都是屬性為主,JQ基本上都方法為主。 可通過 console.dir() 在控制台輸出對象的所有屬性和方法區別。 JQ 對象輸出 DOM 對象輸出 JQ事件綁定方式 ...
  • 自學VS第一天 (目標用vs做個不low的簡歷) 學習視頻 代碼 寫了一天的代碼,自己理解的內容在註釋里 完成效果 VS的漢化 在插件欄找到Chinese (Simplified) Language Pack for Visual Studio Code進行安裝重啟軟體即可 VS我用到的插件 Bra ...
  • 我們正常封裝一個相容的綁定事件函數會這樣寫: 看起來沒什麼問題, 但是, 既然我們封裝了這樣一個函數, 那我們肯定會頻繁調用它, 每次調用都走一次if 判斷 , 性能就會降低, 那我們就要想一個辦法 , 只在第一次調用時判斷一次, 後面再次調用就不用判斷了, 這就是惰性函數的用法: 直接在函數內部重 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...