事件是JavaScript中最重要的一個特征,nodejs就是利用js這一異步而設計出來的。所以這裡講一下事件機制。
在一個js文件中,如果要運行某一個函數,有2中手段,一個就是直接調用,比如foo(),第二就是利用事件來觸發,這中函數也叫回調函數,比如傳遞給setTimeout函數和onready屬性。
setTimeout本質上也是一種異步事件,當延遲時間到的時候觸發該事件,但是有的有的時候(其實也是大部分時候)都不會按照給定的延遲時間執行,先看下面的代碼
var start = new Date(); setTimeout(function() { console.log('settimeout1:',new Date()-start); }, 500); while (new Date() - start < 1000) { console.log('in while'); } document.getElementById('test').addEventListener('click', function(){ console.log('test:',new Date()-start); }, false) for(var i=0;i<10000;i++){ console.log('in for'); } setTimeout(function(){ console.log('settimeout2: ',new Date()-start); },1000); /* 10214 in while index.jsp (第 19 行) in for index.jsp (第 25 行) settimeout1: 2263 index.jsp (第 16 行) settimeout2: 3239 index.jsp (第 28 行) test: 10006 index.jsp (第 22 行) test: 28175 index.jsp (第 22 行) test: 28791 index.jsp (第 22 行) test: 28966 index.jsp (第 22 行) */
如果按照正常的理解,延遲函數應該在500毫秒之後打斷while循環,而事實上並沒有,並且,我在while循環和for循環期間點擊div時候並沒有立即輸出test,給出的解釋就是:
a)事件隊列。調用setTimeout函數的時候,會把傳入它的回調函數加入到事件隊列中去(事件已經初始化並且在內存了),然後繼續執行後面的代碼,直到再也沒有代碼可以運行(沒有正常的運行流了,不包括事件函數等異步的內容),就會從事件隊列裡面pop出一個合適的事件來運行。
b)js是單線程的,事件處理器在線程空閒之前是不會運行的。
promise是一種解決ajax等異步編程回調函數嵌套太多導致代碼晦澀難懂的解決方案,特別是在nodejs中,異步無處不在。不同的框架對promise的實現,一下是jquery中的promise的API。
這裡不講promise的實現原理,關於原理在另外的篇幅中介紹。
傳統的ajax異步編程是這麼寫的(jquery1.5之前):
$.get('url', function(){ $.get('url1', function(){ $.get('url2', function(){ }, 'json'); }, 'json'); }, 'json');
這麼寫代碼給開發和維護帶來了極大的困難,好在jquery1.5以後引入了promise,就可以這麼寫了:
$.ajax( "example.php" ) .done(function() { alert("success"); }) .fail(function() { alert("error"); }) .always(function() { alert("complete"); });
現在看上去就明顯簡單多了。
var nanowrimoing = $.Deferred(); var wordGoal = 5000; nanowrimoing.progress(function(wordCount) { var percentComplete = Math.floor(wordCount / wordGoal * 100); $('#indicator').text(percentComplete + '% complete'); }); nanowrimoing.done(function(){ $('#indicator').text('Good job!'); });
腳本分為兩大類:阻塞式和非阻塞式。這裡的阻塞是指加載阻塞而不是運行阻塞。
<!DOCTYPE html> <html> <head> <script src="headScript"></script> <script defer src="deferredScript"></script> </head> <body> <script async defer src="chatWidget"></script> <script async defer src="asyncScript"></script> </body> </html>
上面這部分代碼是比較標准的關於腳本在一個頁面中的位置,1.其中傳統的未加任何修飾的headScript是阻塞式的腳本,由於浏覽器從上到下解釋執行JavaScript,所以這部分腳本文件在一開始就會被執行,並且在執行完之前是DOM是不會渲染的,但是head標簽裡面的css會加載。2.有defer屬性的腳本會在DOM渲染的同時進行加載,但是會在DOM渲染完畢之後才開始執行,不幸的是,不是所有的浏覽器都支持defer屬性,所以才會有了jquery(function)這個東西。3.同時帶有async屬性和defer屬性時候,defer會覆蓋async,但是單獨有async的時候,腳本會在DOM渲染的時候加載並且運行。
如果不是一開始就在頁面種引入js文件,而是通過用戶交互來實現動態的加載js腳本,可以通過編程方式加入。
浏覽器獲取服務器腳本有2個方法,ajax獲取並且通過eval函數執行,另外一個就是在DOM中插入<script>標簽,一般用第二種方法,因為浏覽器幫助我們生成HTTP請求以及eval會洩露作用域。
var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.src = '/js/feature.js'; head.appendChild(script);
script.onload = function() {
// 現在可以調用腳本裡定義的函數了
}
不過,一些老版本的浏覽器不支持onloa事件,所以可以使用一些腳本加載庫來實現這一功能,如require.js。