引言
俗話說的好,不喜歡研究原理的程序員不是好的程序員,不喜歡讀源碼的程序員不是好的 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/;
第二步:下載文件;
第三步:在頁面中引入 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 一樣。
定義模塊:
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 );
導(dǎo)入模塊:
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 });
關(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ì)輸出:
但是如果是動(dòng)態(tài)加載腳本的話,腳本的執(zhí)行順序是異步的,而且不光是異步的,還是無序的:
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");
使用這種方式加載腳本會(huì)造成腳本的無序加載,瀏覽器按照先來先運(yùn)行的方法執(zhí)行腳本,如果 module1.js 文件比較大,那么極其有可能會(huì)在 module2.js 和 module3.js 后執(zhí)行,所以說這也是不可控的。要知道一個(gè)程序當(dāng)中最大的 BUG 就是一個(gè)不可控的 BUG ,有時(shí)候它可能按順序執(zhí)行,有時(shí)候它可能亂序,這一定不是我們想要的。
注意這里的還有一個(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 的源碼:
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 導(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ù):
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;
注意在這段源碼當(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ì)是這樣:
相信細(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ù)的方式檢查模塊是否加載完全:
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 }
實(shí)驗(yàn)一下是否能夠工作:
1 myRequire(["module/module1.js", "module/module2.js", "module/module3.js"], function(){2 console.log("ready!");3 });
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