從 RequireJs 源碼剖析腳本載入原理

来源:http://www.cnblogs.com/dong-xu/archive/2017/07/13/7160919.html
-Advertisement-
Play Games

本文不是一篇 JavaScript 模塊化或者 RequireJS 的教程,只是從 RequireJS 的源碼來剖析了動態載入腳本和處理非同步的思想,如果你想閱讀一篇有關 RequireJS API 的文章,那麼這並不適合你,如果你對 RequireJS 載入腳本和處理回調的原理感興趣,那麼本篇文章一... ...


 引言

  俗話說的好,不喜歡研究原理的程式員不是好的程式員,不喜歡讀源碼的程式員不是好的 jser。這兩天看到了有關前端模塊化的問題,才發現 JavaScript 社區為了前端工程化真是煞費苦心。今天研究了一天前端模塊化的問題,先是大概瞭解了下模塊化的標準規範,然後瞭解了一下 RequireJs 的語法和使用方法,最後研究了下 RequireJs 的設計模式和源碼,所以想記錄一下相關的心得,剖析一下模塊載入的原理。

 一、認識 RequireJs

   在開始之前,我們需要瞭解前端模塊化,本文不討論有關前端模塊化的問題,有關這方面的問題可以參考阮一峰的系列文章 Javascript 模塊化編程

  使用 RequireJs 的第一步:前往官網 http://requirejs.org/

  第二步:下載文件;

  

  

 

   第三步:在頁面中引入 requirejs.js 並設置 main 函數;

1 <script type="text/javascript" src="scripts/require.js" data-main="scripts/main.js"></script>

  然後我們就可以在 main.js 文件里編程了,requirejs 採用了 main 函數式的思想,一個文件即為一個模塊,模塊與模塊之間可以依賴,也可以毫無干系。使用 requirejs ,我們在編程時就不必將所有模塊都引入頁面,而是需要一個模塊,引入一個模塊,就相當於 Java 當中的 import 一樣。

  定義模塊:

 1 //直接定義一個對象
 2 define({
 3     color: "black",
 4     size: "unisize"
 5 });
 6 //通過函數返回一個對象,即可以實現 IIFE
 7 define(function () {
 8     //Do setup work here
 9 
10     return {
11         color: "black",
12         size: "unisize"
13     }
14 });
15 //定義有依賴項的模塊
16 define(["./cart", "./inventory"], function(cart, inventory) {
17         //return an object to define the "my/shirt" module.
18         return {
19             color: "blue",
20             size: "large",
21             addToCart: function() {
22                 inventory.decrement(this);
23                 cart.add(this);
24             }
25         }
26     }
27 );

  導入模塊:

1 //導入一個模塊
2 require(['foo'], function(foo) {
3     //do something
4 });
5 //導入多個模塊
6 require(['foo', 'bar'], function(foo, bar) {
7     //do something
8 });

  關於 requirejs 的使用,可以查看官網 API ,也可以參考 RequireJS 和 AMD 規範 ,本文暫不對 requirejs 的使用進行講解。

 二、main 函數入口

  requirejs 的核心思想之一就是使用一個規定的函數入口,就像 C++ 的 int main(),Java 的 public static void main(),requirejs 的使用方式是把 main 函數緩存在 script 標簽上。也就是將腳本文件的 url 緩存在 script 標簽上。

1 <script type="text/javascript" src="scripts/require.js" data-main="scripts/main.js"></script>

  初來乍到電腦同學一看,哇!script 標簽難道還有什麼不為人知的屬性嗎?嚇得我趕緊打開了 W3C 查看相關 API,併為自己的 HTML 基礎知識感到慚愧,可是遺憾的是 script 標簽並沒有相關的屬性,甚至這都不是一個標準的屬性,那麼它到底是什麼玩意呢?下麵直接上一部分 requirejs 源碼:

1 //Look for a data-main attribute to set main script for the page
2 //to load. If it is there, the path to data main becomes the
3 //baseUrl, if it is not already set.
4 dataMain = script.getAttribute('data-main');

  實際上在 requirejs 中只是獲取在 script 標簽上緩存的數據,然後取出數據載入而已,也就是跟動態載入腳本是一樣的,具體是怎麼操作,在下麵的講解中會放出源碼。

 三、動態載入腳本

  這一部分是整個 requirejs 的核心,我們知道在 Node.js 中載入模塊的方式是同步的,這是因為在伺服器端所有文件都存儲在本地的硬碟上,傳輸速率快而且穩定。而換做了瀏覽器端,就不能這麼幹了,因為瀏覽器載入腳本會與伺服器進行通信,這是一個未知的請求,如果使用同步的方式載入,就可能會一直阻塞下去。為了防止瀏覽器的阻塞,我們要使用非同步的方式載入腳本。因為是非同步載入,所以與模塊相依賴的操作就必須得在腳本載入完成後執行,這裡就得使用回調函數的形式。

  我們知道,如果顯示的在 HTML 中定義腳本文件,那麼腳本的執行順序是同步的,比如:

1 //module1.js
2 console.log("module1");
1 //module2.js
2 console.log("module2");
1 //module3.js
2 console.log("module3");
1 <script type="text/javascript" src="scripts/module/module1.js"></script>
2 <script type="text/javascript" src="scripts/module/module2.js"></script>
3 <script type="text/javascript" src="scripts/module/module3.js"></script>

  那麼在瀏覽器端總是會輸出:

  但是如果是動態載入腳本的話,腳本的執行順序是非同步的,而且不光是非同步的,還是無序的

 1 //main.js
 2 console.log("main start");
 3 
 4 var script1 = document.createElement("script");
 5 script1.src = "scripts/module/module1.js";
 6 document.head.appendChild(script1);
 7 
 8 var script2 = document.createElement("script");
 9 script2.src = "scripts/module/module2.js";
10 document.head.appendChild(script2);
11 
12 var script3 = document.createElement("script");
13 script3.src = "scripts/module/module3.js";
14 document.head.appendChild(script3);
15 
16 console.log("main end");

   使用這種方式載入腳本會造成腳本的無序載入,瀏覽器按照先來先運行的方法執行腳本,如果 module1.js 文件比較大,那麼極其有可能會在 module2.js 和 module3.js 後執行,所以說這也是不可控的。要知道一個程式當中最大的 BUG 就是一個不可控的 BUG ,有時候它可能按順序執行,有時候它可能亂序,這一定不是我們想要的。

  註意這裡的還有一個重點是,"module" 的輸出永遠會在 "main end" 之後。這正是動態載入腳本非同步性的特征,因為當前的腳本是一個 task ,而無論其他腳本的載入速度有多快,它都會在 Event Queue 的後面等待調度執行。這裡涉及到一個關鍵的知識 — Event Loop ,如果你還對 JavaScript Event Loop 不瞭解,那麼請先閱讀這篇文章 深入理解 JavaScript 事件迴圈(一)— Event Loop

 四、導入模塊原理

  在上一小節,我們瞭解到,使用動態載入腳本的方式會使腳本無序執行,這一定是軟體開發的噩夢,想象一下你的模塊之間存在上下依賴的關係,而這時候他們的載入順序是不可控的。動態載入同時也具有非同步性,所以在 main.js 腳本文件中根本無法訪問到模塊文件中的任何變數。那麼 requirejs 是如何解決這個問題的呢?我們知道在 requirejs 中,任何文件都是一個模塊,一個模塊也就是一個文件,包括主模塊 main.js,下麵我們看一段 requirejs 的源碼:

 1 /**
 2  * Creates the node for the load command. Only used in browser envs.
 3  */
 4 req.createNode = function (config, moduleName, url) {
 5     var node = config.xhtml ?
 6             document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
 7             document.createElement('script');
 8     node.type = config.scriptType || 'text/javascript';
 9     node.charset = 'utf-8';
10     node.async = true;
11     return node;
12 };

  在這段代碼中我們可以看出, requirejs 導入模塊的方式實際就是創建腳本標簽,一切的模塊都需要經過這個方法創建。那麼 requirejs 又是如何處理非同步載入的呢?傳說江湖上最高深的醫術不是什麼靈丹妙藥,而是以毒攻毒,requirejs 也深得其精髓,既然動態載入是非同步的,那麼我也用非同步來對付你,使用 onload 事件來處理回調函數:

 1 //In the browser so use a script tag
 2 node = req.createNode(config, moduleName, url);
 3 
 4 node.setAttribute('data-requirecontext', context.contextName);
 5 node.setAttribute('data-requiremodule', moduleName);
 6 
 7 //Set up load listener. Test attachEvent first because IE9 has
 8 //a subtle issue in its addEventListener and script onload firings
 9 //that do not match the behavior of all other browsers with
10 //addEventListener support, which fire the onload event for a
11 //script right after the script execution. See:
12 //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
13 //UNFORTUNATELY Opera implements attachEvent but does not follow the script
14 //script execution mode.
15 if (node.attachEvent &&
16     //Check if node.attachEvent is artificially added by custom script or
17     //natively supported by browser
18     //read https://github.com/requirejs/requirejs/issues/187
19     //if we can NOT find [native code] then it must NOT natively supported.
20     //in IE8, node.attachEvent does not have toString()
21     //Note the test for "[native code" with no closing brace, see:
22     //https://github.com/requirejs/requirejs/issues/273
23     !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
24     !isOpera) {
25     //Probably IE. IE (at least 6-8) do not fire
26     //script onload right after executing the script, so
27     //we cannot tie the anonymous define call to a name.
28     //However, IE reports the script as being in 'interactive'
29     //readyState at the time of the define call.
30     useInteractive = true;
31 
32     node.attachEvent('onreadystatechange', context.onScriptLoad);
33     //It would be great to add an error handler here to catch
34     //404s in IE9+. However, onreadystatechange will fire before
35     //the error handler, so that does not help. If addEventListener
36     //is used, then IE will fire error before load, but we cannot
37     //use that pathway given the connect.microsoft.com issue
38     //mentioned above about not doing the 'script execute,
39     //then fire the script load event listener before execute
40     //next script' that other browsers do.
41     //Best hope: IE10 fixes the issues,
42     //and then destroys all installs of IE 6-9.
43     //node.attachEvent('onerror', context.onScriptError);
44 } else {
45     node.addEventListener('load', context.onScriptLoad, false);
46     node.addEventListener('error', context.onScriptError, false);
47 }
48 node.src = url;

  註意在這段源碼當中的監聽事件,既然動態載入腳本是非同步的的,那麼乾脆使用 onload 事件來處理回調函數,這樣就保證了在我們的程式執行前依賴的模塊一定會提前載入完成。因為在事件隊列里, onload 事件是在腳本載入完成之後觸發的,也就是在事件隊列裡面永遠處在依賴模塊的後面,例如我們執行:

1 require(["module"], function (module) {
2     //do something
3 });

  那麼在事件隊列裡面的相對順序會是這樣:

  相信細心的同學可能會註意到了,在源碼當中不光光有 onload 事件,同時還添加了一個  onerror 事件,我們在使用 requirejs 的時候也可以定義一個模塊載入失敗的處理函數,這個函數在底層也就對應了 onerror 事件。同理,其和 onload 事件一樣是一個非同步的事件,同時也永遠發生在模塊載入之後。

  談到這裡 requirejs 的核心模塊思想也就一目瞭然了,不過其中的過程還遠不直這些,博主只是將模塊載入的實現思想拋了出來,但 requirejs 的具體實現還要複雜的多,比如我們定義模塊的時候可以導入依賴模塊,導入模塊的時候還可以導入多個依賴,具體的實現方法我就沒有深究過了, requirejs 雖然不大,但是源碼也是有兩千多行的... ...但是只要理解了動態載入腳本的原理過後,其思想也就不難理解了,比如我現在就可以想到一個簡單的實現多個模塊依賴的方法,使用計數的方式檢查模塊是否載入完全:

 1 function myRequire(deps, callback){
 2     //記錄模塊載入數量
 3     var ready = 0;
 4     //創建腳本標簽
 5     function load (url) {
 6         var script = document.createElement("script");
 7         script.type = 'text/javascript';
 8         script.async = true;
 9         script.src = url;
10         return script;
11     }
12     var nodes = [];
13     for (var i = deps.length - 1; i >= 0; i--) {
14         nodes.push(load(deps[i]));
15     }
16     //載入腳本
17     for (var i = nodes.length - 1; i >= 0; i--) {
18         nodes[i].addEventListener("load", function(event){
19             ready++;
20             //如果所有依賴腳本載入完成,則執行回調函數;
21             if(ready === nodes.length){
22                 callback()
23             }
24         }, false);
25         document.head.appendChild(nodes[i]);
26     }
27 }

  實驗一下是否能夠工作:

1 myRequire(["module/module1.js", "module/module2.js", "module/module3.js"], function(){
2     console.log("ready!");
3 });

 

  Yes, it's work!

 總結

  requirejs 載入模塊的核心思想是利用了動態載入腳本的非同步性以及 onload 事件以毒攻毒,關於腳本的載入,我們需要註意一下幾點:

  •   在 HTML 中引入 <script> 標簽是同步載入;
  •   在腳本中動態載入是非同步載入,且由於被載入的腳本在事件隊列的後端,因此總是會在當前腳本之後執行;
  •   使用 onload 和 onerror 事件可以監聽腳本載入完成,以非同步的事件來處理非同步的事件;

 參考文獻:

  阮一峰 — RequireJS 和 AMD 規範

  阮一峰 — Javascript 模塊化編程

  requirejs.org — requirejs api

 


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

-Advertisement-
Play Games
更多相關文章
  • 前段時間一直忙於期末考試和找實習,好久沒寫博客了。 這段時間做了個小項目,包含了翻頁和富文本編輯器Ueditor的兩個知識點,Ueditor玩的還不是很深,打算玩深後再寫篇博客。 要實現翻頁功能,只需要設置一個pageIndex即可,然後每次載入頁面時通過pageIndex去載入數據就行。 那麼我們 ...
  • 原創: "荊秀網 網頁即時推送 https://xxuyou.com" | 轉載請註明出處 鏈接: "https://blog.xxuyou.com/nodejs thinkjs study config/" 本系列教程以 ThinkJS v2.x 版本( "官網" )為例進行介紹,教程以實際操作為 ...
  • 在常見的用戶註冊頁面,需要用戶在本地選擇一張圖片作為頭像,並同時預覽。 常見的思路有兩種:一是將圖片上傳至伺服器的臨時文件夾中,並返回該圖片的url,然後渲染在html頁面;另一種思路是,直接在本地記憶體中預覽圖片,用戶確認提交後再上傳至伺服器保存。 這兩種方法各有利弊,方法一很明顯,浪費流量和伺服器 ...
  • html概念+三大元素inline,block,inline-block ...
  • 一.概述層疊樣式表;可以對HTML的元素,進行控制,使HTML的元素展現的效果和位置更好;二.基本語法css規則由兩個部分構成:選擇器和語句語句規則:1.css選擇器的名稱區分大小寫;屬性名和屬性值區分大小寫;2.每條語句的結尾都要使用;,最後一行可以省略;3.註釋格式:/**/4.css在html... ...
  • 函數的定義 1.函數的聲明 function 函數名(){ } 2.函數表達式 var aa=function(){ } 函數的調用 1.函數名() 函數分類:方式一: 有名函數 匿名函數(匿名函數無法直接調用,如果想要調用,需要使用匿名函數的自調用) ( function(){ alert(); ...
  • 首先,取值有以下兩種方式: 1:$('#com').combobox('getValue') 2:$('#com').combobox('getText) 區別就不說了。 作為選擇觸發事件,比較之後發現: onselect:事件觸發之後,獲取到的是改變之前的值 onsuncess:獲取到的是改變之後 ...
  • 緩存變數 DOM遍歷是昂貴的,所以儘量將會重用的元素緩存。 避免全局變數 jQuery與javascript一樣,一般來說,最好確保你的變數在函數作用域內。 使用匈牙利命名法 在變數前加$首碼,便於識別出jQuery對象。 使用 Var 鏈(單 Var 模式) 將多條var語句合併為一條語句,我建議 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...