俗話說的好,不喜歡研究原理的程序員不是好的程序員,不喜歡讀源碼的程序員不是好的 jser。這兩天看到了有關前端模塊化的問題,才發現 JavaScript 社區爲了前端工程化真是煞費苦心。今天研究了一天前端模塊化的問題,先是大概瞭解了下模塊化的標準規範,而後瞭解了一下 RequireJs 的語法和使用方法,最後研究了下 RequireJs 的設計模式和源碼,因此想記錄一下相關的心得,剖析一下模塊加載的原理。javascript
在開始以前,咱們須要瞭解前端模塊化,本文不討論有關前端模塊化的問題,有關這方面的問題能夠參考阮一峯的系列文章 Javascript 模塊化編程。html
使用 RequireJs 的第一步:前往官網 http://requirejs.org/;前端
第二步:下載文件;java
第三步:在頁面中引入 requirejs.js 並設置 main 函數;node
1 <script type="text/javascript" src="scripts/require.js" data-main="scripts/main.js"></script>
而後咱們就能夠在 main.js 文件裏編程了,requirejs 採用了 main 函數式的思想,一個文件即爲一個模塊,模塊與模塊之間能夠依賴,也能夠毫無干系。使用 requirejs ,咱們在編程時就沒必要將全部模塊都引入頁面,而是須要一個模塊,引入一個模塊,就至關於 Java 當中的 import 同樣。git
定義模塊:程序員
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 );
導入模塊:github
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 的使用進行講解。編程
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 事件以毒攻毒,關於腳本的加載,咱們須要注意一下幾點:
阮一峯 — RequireJS 和 AMD 規範
阮一峯 — Javascript 模塊化編程
requirejs.org — requirejs api