1. 程式人生 > >手把手教你造一個簡易的require.js

手把手教你造一個簡易的require.js

pic0.pngspacer.gif

示例程式碼託管在我的程式碼倉:http://www.github.com/dashnowords/blogs

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

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

javascript基礎修煉(12)——手把手教你造一個簡易的require.js一. 概述二. require.js2.1 基本用法2.2 細說API設計三. 造輪子3.1 模組載入執行的步驟3.2 程式碼框架3.3 關鍵函式的程式碼實現

一. 概述

許多前端工程師沉浸在使用腳手架工具的快感中,認為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):

pic1.PNGspacer.gif

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。如下所示:

pic2.pngspacer.gif

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

購買華為雲請