引言

  俗話說的好,不喜歡研究原理的程序員不是好的程序員,不喜歡讀源碼的程序員不是好的 jser。這兩天看到了有關(guān)前端模塊化的問題,才發(fā)現(xiàn) JavaScript 社區(qū)為了前端工程化真是煞費(fèi)苦心。今天研究了一天前端模塊化的問題,先是大概了解了下模塊化的標(biāo)準(zhǔn)規(guī)范,然后了解了一下 RequireJs 的語法和使用方法,最后研究了下 RequireJs 的設(shè)計(jì)模式和源碼,所以想記錄一下相關(guān)的心得,剖析一下模塊加載的原理。

 一、認(rèn)識(shí) RequireJs

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

  使用 RequireJs 的第一步:前往官網(wǎng) http://requirejs.org/

  第二步:下載文件;

  photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 

   第三步:在頁面中引入 requirejs.js 并設(shè)置 main 函數(shù);

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

  然后我們就可以在 main.js 文件里編程了,requirejs 采用了 main 函數(shù)式的思想,一個(gè)文件即為一個(gè)模塊,模塊與模塊之間可以依賴,也可以毫無干系。使用 requirejs ,我們?cè)诰幊虝r(shí)就不必將所有模塊都引入頁面,而是需要一個(gè)模塊,引入一個(gè)模塊,就相當(dāng)于 Java 當(dāng)中的 import 一樣。

  定義模塊:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 1 //直接定義一個(gè)對(duì)象 2 define({ 3     color: "black", 4     size: "unisize" 5 }); 6 //通過函數(shù)返回一個(gè)對(duì)象,即可以實(shí)現(xiàn) IIFE 7 define(function () { 8     //Do setup work here 9 10     return {11         color: "black",12         size: "unisize"13     }14 });15 //定義有依賴項(xiàng)的模塊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 );

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  導(dǎo)入模塊:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

1 //導(dǎo)入一個(gè)模塊2 require(['foo'], function(foo) {3     //do something4 });5 //導(dǎo)入多個(gè)模塊6 require(['foo', 'bar'], function(foo, bar) {7     //do something8 });

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  關(guān)于 requirejs 的使用,可以查看官網(wǎng) API ,也可以參考 RequireJS 和 AMD 規(guī)范 ,本文暫不對(duì) requirejs 的使用進(jìn)行講解。

 二、main 函數(shù)入口

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

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

  初來乍到電腦同學(xué)一看,哇!script 標(biāo)簽難道還有什么不為人知的屬性嗎?嚇得我趕緊打開了 W3C 查看相關(guān) API,并為自己的 HTML 基礎(chǔ)知識(shí)感到慚愧,可是遺憾的是 script 標(biāo)簽并沒有相關(guān)的屬性,甚至這都不是一個(gè)標(biāo)準(zhǔn)的屬性,那么它到底是什么玩意呢?下面直接上一部分 requirejs 源碼:

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

  實(shí)際上在 requirejs 中只是獲取在 script 標(biāo)簽上緩存的數(shù)據(jù),然后取出數(shù)據(jù)加載而已,也就是跟動(dòng)態(tài)加載腳本是一樣的,具體是怎么操作,在下面的講解中會(huì)放出源碼。

 三、動(dòng)態(tài)加載腳本

  這一部分是整個(gè) requirejs 的核心,我們知道在 Node.js 中加載模塊的方式是同步的,這是因?yàn)樵诜?wù)器端所有文件都存儲(chǔ)在本地的硬盤上,傳輸速率快而且穩(wěn)定。而換做了瀏覽器端,就不能這么干了,因?yàn)闉g覽器加載腳本會(huì)與服務(wù)器進(jìn)行通信,這是一個(gè)未知的請(qǐng)求,如果使用同步的方式加載,就可能會(huì)一直阻塞下去。為了防止瀏覽器的阻塞,我們要使用異步的方式加載腳本。因?yàn)槭钱惒郊虞d,所以與模塊相依賴的操作就必須得在腳本加載完成后執(zhí)行,這里就得使用回調(diào)函數(shù)的形式。

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

1 //module1.js2 console.log("module1");
1 //module2.js2 console.log("module2");
1 //module3.js2 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>

  那么在瀏覽器端總是會(huì)輸出:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  但是如果是動(dòng)態(tài)加載腳本的話,腳本的執(zhí)行順序是異步的,而且不光是異步的,還是無序的

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 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");

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

   使用這種方式加載腳本會(huì)造成腳本的無序加載,瀏覽器按照先來先運(yùn)行的方法執(zhí)行腳本,如果 module1.js 文件比較大,那么極其有可能會(huì)在 module2.js 和 module3.js 后執(zhí)行,所以說這也是不可控的。要知道一個(gè)程序當(dāng)中最大的 BUG 就是一個(gè)不可控的 BUG ,有時(shí)候它可能按順序執(zhí)行,有時(shí)候它可能亂序,這一定不是我們想要的。

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  注意這里的還有一個(gè)重點(diǎn)是,"module" 的輸出永遠(yuǎn)會(huì)在 "main end" 之后。這正是動(dòng)態(tài)加載腳本異步性的特征,因?yàn)楫?dāng)前的腳本是一個(gè) task ,而無論其他腳本的加載速度有多快,它都會(huì)在 Event Queue 的后面等待調(diào)度執(zhí)行。這里涉及到一個(gè)關(guān)鍵的知識(shí) — Event Loop ,如果你還對(duì) JavaScript Event Loop 不了解,那么請(qǐng)先閱讀這篇文章 深入理解 JavaScript 事件循環(huán)(一)— Event Loop。

 四、導(dǎo)入模塊原理

  在上一小節(jié),我們了解到,使用動(dòng)態(tài)加載腳本的方式會(huì)使腳本無序執(zhí)行,這一定是軟件開發(fā)的噩夢(mèng),想象一下你的模塊之間存在上下依賴的關(guān)系,而這時(shí)候他們的加載順序是不可控的。動(dòng)態(tài)加載同時(shí)也具有異步性,所以在 main.js 腳本文件中根本無法訪問到模塊文件中的任何變量。那么 requirejs 是如何解決這個(gè)問題的呢?我們知道在 requirejs 中,任何文件都是一個(gè)模塊,一個(gè)模塊也就是一個(gè)文件,包括主模塊 main.js,下面我們看一段 requirejs 的源碼:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 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 };

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  在這段代碼中我們可以看出, requirejs 導(dǎo)入模塊的方式實(shí)際就是創(chuàng)建腳本標(biāo)簽,一切的模塊都需要經(jīng)過這個(gè)方法創(chuàng)建。那么 requirejs 又是如何處理異步加載的呢?傳說江湖上最高深的醫(yī)術(shù)不是什么靈丹妙藥,而是以毒攻毒,requirejs 也深得其精髓,既然動(dòng)態(tài)加載是異步的,那么我也用異步來對(duì)付你,使用 onload 事件來處理回調(diào)函數(shù):

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 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 with10 //addEventListener support, which fire the onload event for a11 //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-execution13 //UNFORTUNATELY Opera implements attachEvent but does not follow the script14 //script execution mode.15 if (node.attachEvent &&16     //Check if node.attachEvent is artificially added by custom script or17     //natively supported by browser18     //read https://github.com/requirejs/requirejs/issues/18719     //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/27323     !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&24     !isOpera) {25     //Probably IE. IE (at least 6-8) do not fire26     //script onload right after executing the script, so27     //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 catch34     //404s in IE9+. However, onreadystatechange will fire before35     //the error handler, so that does not help. If addEventListener36     //is used, then IE will fire error before load, but we cannot37     //use that pathway given the connect.microsoft.com issue38     //mentioned above about not doing the 'script execute,39     //then fire the script load event listener before execute40     //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;

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  注意在這段源碼當(dāng)中的監(jiān)聽事件,既然動(dòng)態(tài)加載腳本是異步的的,那么干脆使用 onload 事件來處理回調(diào)函數(shù),這樣就保證了在我們的程序執(zhí)行前依賴的模塊一定會(huì)提前加載完成。因?yàn)樵谑录?duì)列里, onload 事件是在腳本加載完成之后觸發(fā)的,也就是在事件隊(duì)列里面永遠(yuǎn)處在依賴模塊的后面,例如我們執(zhí)行:

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

  那么在事件隊(duì)列里面的相對(duì)順序會(huì)是這樣:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  相信細(xì)心的同學(xué)可能會(huì)注意到了,在源碼當(dāng)中不光光有 onload 事件,同時(shí)還添加了一個(gè)  onerror 事件,我們?cè)谑褂?requirejs 的時(shí)候也可以定義一個(gè)模塊加載失敗的處理函數(shù),這個(gè)函數(shù)在底層也就對(duì)應(yīng)了 onerror 事件。同理,其和 onload 事件一樣是一個(gè)異步的事件,同時(shí)也永遠(yuǎn)發(fā)生在模塊加載之后。

  談到這里 requirejs 的核心模塊思想也就一目了然了,不過其中的過程還遠(yuǎn)不直這些,博主只是將模塊加載的實(shí)現(xiàn)思想拋了出來,但 requirejs 的具體實(shí)現(xiàn)還要復(fù)雜的多,比如我們定義模塊的時(shí)候可以導(dǎo)入依賴模塊,導(dǎo)入模塊的時(shí)候還可以導(dǎo)入多個(gè)依賴,具體的實(shí)現(xiàn)方法我就沒有深究過了, requirejs 雖然不大,但是源碼也是有兩千多行的... ...但是只要理解了動(dòng)態(tài)加載腳本的原理過后,其思想也就不難理解了,比如我現(xiàn)在就可以想到一個(gè)簡(jiǎn)單的實(shí)現(xiàn)多個(gè)模塊依賴的方法,使用計(jì)數(shù)的方式檢查模塊是否加載完全:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 1 function myRequire(deps, callback){ 2     //記錄模塊加載數(shù)量 3     var ready = 0; 4     //創(chuàng)建腳本標(biāo)簽 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             //如果所有依賴腳本加載完成,則執(zhí)行回調(diào)函數(shù);21             if(ready === nodes.length){22                 callback()23             }24         }, false);25         document.head.appendChild(nodes[i]);26     }27 }

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  實(shí)驗(yàn)一下是否能夠工作:

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

 photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  Yes, it's work!

 總結(jié)

  requirejs 加載模塊的核心思想是利用了動(dòng)態(tài)加載腳本的異步性以及 onload 事件以毒攻毒,關(guān)于腳本的加載,我們需要注意一下幾點(diǎn):

  •   在 HTML 中引入 <script> 標(biāo)簽是同步加載;

  •   在腳本中動(dòng)態(tài)加載是異步加載,且由于被加載的腳本在事件隊(duì)列的后端,因此總是會(huì)在當(dāng)前腳本之后執(zhí)行;

  •   使用 onload 和 onerror 事件可以監(jiān)聽腳本加載完成,以異步的事件來處理異步的事件;

 參考文獻(xiàn):

  阮一峰 — RequireJS 和 AMD 規(guī)范

  阮一峰 — Javascript 模塊化編程

  requirejs.org — requirejs api

http://www.cnblogs.com/dong-xu/p/7160919.html