JavaScript異步編程教學(xué)_第1頁
JavaScript異步編程教學(xué)_第2頁
JavaScript異步編程教學(xué)_第3頁
JavaScript異步編程教學(xué)_第4頁
JavaScript異步編程教學(xué)_第5頁
已閱讀5頁,還剩114頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

請認(rèn)真閱讀后確認(rèn)是否使用~PAGEPAGE118YOURNAME-YOURNAMEJavaScript異步編程JavaScript異步編程教學(xué)目錄|1第1章目錄第1章深入理解JavaScript事件11.1事件的調(diào)度21.1.3現(xiàn)在還是將來運(yùn)行線程的阻塞隊(duì)列341.21.3異步函數(shù)的類型51.2.1異步的I/O函數(shù)51.2.2異步的計(jì)時(shí)函數(shù)7異步函數(shù)的編寫10101112141518182023..41.3.5何時(shí)稱函數(shù)為異步的間或異步的函數(shù)緩存型異步函數(shù)異步遞歸與回調(diào)存儲返值與回調(diào)的混搭1.41.5異步錯(cuò)誤的處理21.4.3回調(diào)內(nèi)拋出的錯(cuò)誤未捕獲異常的處理拋出還是不拋出嵌套式回調(diào)的解嵌套2|目錄1.6小結(jié)2627第2章分布式事件2.12.2PubSub模式283031323435363EventEmitter對象玩轉(zhuǎn)自己的PubSub同步性事件化模型2.2.1模型事件的傳播事件循環(huán)與嵌套式變化2.4jQuery自定義事件小結(jié)第3章Promise對象和Deferred對象433.13.2Promise極簡史生成Promise對象45464850525355586263653.2.1生成純Promise對象jQueryAPI中的Promise對象向回調(diào)傳遞數(shù)據(jù)進(jìn)度通知Promise對象的合并管道連接未來jQuery與Promises/A的對比用Promise對象代替回調(diào)函數(shù)小結(jié)第4章Async.js的工作流控制67684.1異步工作流的次序問題(

目錄|異步的數(shù)據(jù)收集方法707173757577787880818283844.2.1Async.js的函數(shù)式寫法Async.js的錯(cuò)誤處理技術(shù)4.2.2Async.js的任務(wù)組織技術(shù)4.3.1異步函數(shù)序列的運(yùn)行異步函數(shù)的并行運(yùn)行4.3.2異步工作流的動態(tài)排隊(duì)技術(shù)4深入理解隊(duì)列任務(wù)的入列完工事件的處理隊(duì)列的高級回調(diào)方法4.54.6極簡主義者Step的工作流控制小結(jié)第5章worker對象的多線程技術(shù)8網(wǎng)頁版worker對象899091929495965.1.1網(wǎng)頁版worker對象的局限性支持網(wǎng)頁版worker的瀏覽器5.1.2cluster帶來的Node版worker5.2.1Node版worker的交互接口Node版worker對象的局限性5.2.2小結(jié)第6章異步的腳本加載976.16.2局限性與補(bǔ)充說明<script>標(biāo)簽的再認(rèn)識989阻塞型腳本何去何從腳本的延遲運(yùn)行腳本的完全并行化101102(

4|目錄6.3可編程的腳本加載10510510610直接加載腳本yepnope的條件加載Require.js/AMD的智能加載6.4小結(jié)附錄JavaScript編輯工具113118索引(

1.1事件的調(diào)度|1第1章深入理解JavaScript事件事件!事件到底是怎么工作的?JavaScript出現(xiàn)了多久,對JavaScript異步事件模型就迷惘了多久。迷惘導(dǎo)致bug,bug導(dǎo)致憤怒,然后尤達(dá)大師就會教我們?nèi)绾稳绾巍贿^本質(zhì)上,從概念上看,JavaScript事件模型既優(yōu)雅又實(shí)用。一旦大家接受了這種語言的單線程設(shè)計(jì),就會覺得JavaScript事件模型更像是一種功能,而不是一種局限。它意味著我們的代碼是不可中斷的,也意味著調(diào)度的事件會整整齊齊排好隊(duì),有條不紊地運(yùn)行。本章將介紹JavaScript的異步機(jī)制,并破除一些常見的誤解。我們會看到setTimeout真正做了些什么。接著會討論回調(diào)中拋出錯(cuò)誤的處理。最后會奠定本書的主旨:為了清晰和可維護(hù)性,努力組織異步代碼。1.1事件的調(diào)度如果想讓JavaScript中的某段代碼將來再運(yùn)行,可以將它放在回調(diào)中?;卣{(diào)就是一種普通函數(shù),只不過它是傳給像setTimeout這樣的函數(shù),或者綁定為像document.onready這樣的屬性。運(yùn)行回調(diào)時(shí),(2|第1章深入理解JavaScript事件我們稱已觸發(fā)某事件(譬如延時(shí)結(jié)束或頁面加載完畢)。當(dāng)然,可怕的總是那些細(xì)節(jié),哪怕是像setTimeout這樣看起來很簡單的東西。對setTimeout的描述通常像這樣:給定一個(gè)回調(diào)及n毫秒的延遲,setTimeout就會在n毫秒后運(yùn)行該回調(diào)。但是,正如我們將在這一節(jié)乃至這一章里看到的,以上描述存在嚴(yán)重缺陷。大多數(shù)情況下,該描述只能算接近正確,而在其他情況下則完全是謬誤。要想真正理解setTimeout,必須先大體理解JavaScript事件模型。1.1.1現(xiàn)在還是將來運(yùn)行在探究setTimeout之前,先來看一個(gè)簡單的例子。該情形常常會迷惑JavaScript新手,特別是那些剛剛從Java和Ruby等多線程語言遷移過來的新手。EventModel/loopWithTimeout.jsfor(vari=1;i<=3;i++){setTimeout(function(){console.log(i);},0);};444大多數(shù)剛接觸JavaScript語言的人都會認(rèn)為以上循環(huán)會輸出1,2,3,或者重復(fù)輸出這3個(gè)數(shù)字,因?yàn)檫@里的3次延時(shí)都搶著要第一個(gè)觸發(fā)(每次暫停都調(diào)度為0毫秒后到時(shí))。(1.1事件的調(diào)度|3要理解為什么輸出是4,4,4,需要知道以下3件事。這里只有一個(gè)名為i的變量,其作用域由聲明語句vari定義(該聲明語句在不經(jīng)意間讓i的作用域不是循環(huán)內(nèi)部,而是擴(kuò)散至蘊(yùn)含循環(huán)的那個(gè)最內(nèi)側(cè)函數(shù))。循環(huán)結(jié)束后,i===4一直遞增,直到不再滿足條件i<=3為止。JavaScript事件處理器在線程空閑之前不會運(yùn)行。前兩條還屬于JavaScript101的范疇,但第三個(gè)更像是一個(gè)“驚喜”。一開始使用JavaScript的時(shí)候,我也不太相信會這樣。Java令我擔(dān)心自己的代碼隨時(shí)會被中斷。上百萬種潛在的邊界情況讓我焦慮萬分,我一直在想:“要是在這兩行代碼之間發(fā)生了什么稀奇古怪的事,會怎么樣呢?”然后,終于有一天,我再也沒有這樣的擔(dān)心了……1.1.2線程的阻塞下面這段代碼打破了我對JavaScript事件的成見。EventModel/loopBlockingTimeout.jsvarstart=newDate;setTimeout(function(){varend=newDate;console.log('Timeelapsed:',end-start,'ms');},500);while(newDate-start<1000){};按照多線程的思維定勢,我會預(yù)計(jì)500毫秒后計(jì)時(shí)函數(shù)就會運(yùn)行。不過這要求中斷欲持續(xù)整整一秒鐘的循環(huán)。如果運(yùn)行代碼,會得到類似這樣的結(jié)果:(4|第1章深入理解JavaScript事件Timeelapsed:1002ms大家得到的數(shù)字可能會稍有不同,這是因?yàn)閟etTimeout和setIn-terval一樣,其計(jì)時(shí)精度要比我們的期望值差很多(請參閱1.2.2節(jié))。不過,這個(gè)數(shù)字肯定至少是1000,因?yàn)閟etTimeout回調(diào)在while循環(huán)結(jié)束運(yùn)行之前不可能被觸發(fā)。那么,如果setTimeout沒有使用另一個(gè)線程,那它到底在做什么呢?1.1.3隊(duì)列調(diào)用setTimeout的時(shí)候,會有一個(gè)延時(shí)事件排入隊(duì)列。然后setTimeout調(diào)用之后的那行代碼運(yùn)行,接著是再下一行代碼,直到再也沒有任何代碼。這時(shí)JavaScript虛擬機(jī)才會問:“隊(duì)列里都有誰???”如果隊(duì)列中至少有一個(gè)事件適合于“觸發(fā)”(就像1000毫秒之前設(shè)定好的那個(gè)為期500毫秒的延時(shí)事件),則虛擬機(jī)會挑選一個(gè)事件,并調(diào)用此事件的處理器(譬如傳給setTimeout的那個(gè)函數(shù))。事件處理器返回后,我們又回到隊(duì)列處。輸入事件的工作方式完全一樣:用戶單擊一個(gè)已附加有單擊事件處理器的DOM(DocumentObjectModel,文檔對象模型)元素時(shí),會有一個(gè)單擊事件排入隊(duì)列。但是,該單擊事件處理器要等到當(dāng)前所有正在運(yùn)行的代碼均已結(jié)束后(可能還要等其他此前已排隊(duì)的事件也依次結(jié)束)才會執(zhí)行。因此,使用JavaScript的那些網(wǎng)頁一不小心就會變得毫無反應(yīng)。你可能聽過事件循環(huán)這個(gè)術(shù)語,它是用于描述隊(duì)列工作方式的。所謂(1.2異步函數(shù)的類型|5事件循環(huán),就像代碼從一個(gè)循環(huán)中不斷取出而運(yùn)行一樣:runYourScript();while(atLeastOneEventIsQueued){fireNextQueuedEvent();};這隱含著一個(gè)意思,即觸發(fā)的每個(gè)事件都會位于堆棧軌跡的底部。關(guān)于這一點(diǎn),1.4節(jié)會進(jìn)一步闡述。事件的易調(diào)度性是JavaScript語言最大的特色之一。像setTimeout這樣的異步函數(shù)只是簡單地做延遲執(zhí)行,而不是孵化新的線程。JavaScript代碼永遠(yuǎn)不會被中斷,這是因?yàn)榇a在運(yùn)行期間只需要排隊(duì)事件即可,而這些事件在代碼運(yùn)行結(jié)束之前不會被觸發(fā)。下一節(jié)將更細(xì)致地考查異步JavaScript代碼的構(gòu)造塊。1.2異步函數(shù)的類型每一種JavaScript環(huán)境都有自己的異步函數(shù)集。有些函數(shù),如setTimeout和setInterval,是各種JavaScript環(huán)境普遍都有的。另一些函數(shù)則專屬于某些瀏覽器或某幾種服務(wù)器端框架。JavaScript環(huán)境提供的異步函數(shù)通??梢苑譃閮纱箢悾篒/O函數(shù)和計(jì)時(shí)函數(shù)。如果想在應(yīng)用中定義復(fù)雜的異步行為,就要使用這兩類異步函數(shù)作為基本的構(gòu)造塊。1.2.1異步的I/O函數(shù)創(chuàng)造Node.js,并不是為了人們能在服務(wù)器上運(yùn)行JavaScript,僅僅是因?yàn)镽yanDahl想要一個(gè)建立在某高級語言之上的事件驅(qū)動型服務(wù)器(

6|第1章深入理解JavaScript事件框架。JavaScript碰巧就是適合干這個(gè)的語言。為什么?因?yàn)镴avaScript語言可以完美地實(shí)現(xiàn)非阻塞式I/O。在其他語言中,一不小心就會“阻塞”應(yīng)用(通常是運(yùn)行循環(huán))直到完成I/O請求為止。而在JavaScript中,這種阻塞方式幾乎淪為無稽之談。類似如下的循環(huán)將永遠(yuǎn)運(yùn)行下去,不可能停下來。varajaxRequest=newXMLHttpRequest;ajaxRequest.open('GET',url);ajaxRequest.send(null);while(ajaxRequest.readyState===XMLHttpRequest.UNSENT){//readyState在循環(huán)返回之前不會有更改。};相反,我們需要附加一個(gè)事件處理器,隨即返回事件隊(duì)列。varajaxRequest=newXMLHttpRequest;ajaxRequest.open('GET',url);ajaxRequest.send(null);ajaxRequest.onreadystatechange=function(){//...};就是這么回事。不論是在等待用戶的按鍵行為,還是在等待遠(yuǎn)程服務(wù)器的批量數(shù)據(jù),所需要做的就是定義一個(gè)回調(diào),除非JavaScript環(huán)境提供的某個(gè)同步I/O函數(shù)已經(jīng)替我們完成了阻塞。在瀏覽器端,Ajax方法有一個(gè)可設(shè)置為false的async選項(xiàng)(但永遠(yuǎn)、永遠(yuǎn)別這么做),這會掛起整個(gè)瀏覽器窗格直到收到應(yīng)答為止。在Node.js中,同步的API方法在名稱上會有明確的標(biāo)示,譬如fs.readFileSync。編寫短小的腳本時(shí),這些同步方法會很方便。但是,如果所編寫的應(yīng)用需要處理并行的多個(gè)請求或多項(xiàng)操作,則應(yīng)該避免使用它們??稍诮裉?,還有哪個(gè)應(yīng)用不是這樣的呢?(

1.2異步函數(shù)的類型|7有些I/O函數(shù)既有同步效應(yīng),也有異步效應(yīng)。舉例來說,在現(xiàn)代瀏覽器中操縱DOM對象時(shí),從腳本角度看,更改是即時(shí)生效的,但從視效角度看,在返回事件隊(duì)列之前不會渲染這些DOM對象更改。這可以防止DOM對象被渲染成不一致的狀態(tài)。關(guān)于這點(diǎn),可訪問/TrevorBurnham/SNBYV/,查看一個(gè)簡單的演示。console.log是異步的嗎?WebKit的console.log由于表現(xiàn)出異步行為而讓很多開發(fā)者驚詫不已。在Chrome或Safari中,以下這段代碼會在控制臺記錄{foo:bar}。EventModel/log.jsvarobj={};console.log(obj);obj.foo='bar';怎么會這樣?WebKit的console.log并沒有立即拍攝對象快照,相反,它只存儲了一個(gè)指向?qū)ο蟮囊?,然后在代碼返回事件隊(duì)列時(shí)才去拍攝快照。Node的console.log是另一回事,它是嚴(yán)格同步的,因此同樣的代碼輸出的卻為{}。JavaScript采用了非阻塞式I/O,這對新手來說是最大的一個(gè)障礙,但這同樣也是該語言的核心優(yōu)勢之一。有了非阻塞式I/O,就能自然而然地寫出高效的基于事件的代碼。1.2.2異步的計(jì)時(shí)函數(shù)我們已經(jīng)看到,異步函數(shù)非常適合用于I/O操作,但有些時(shí)候,我們僅僅是因?yàn)樾枰惒蕉胍惒叫浴Q句話說,我們想讓一個(gè)函數(shù)在(8|第1章深入理解JavaScript事件將來某個(gè)時(shí)刻再運(yùn)行——這樣的函數(shù)可能是為了作動畫或模擬?;跁r(shí)間的事件涉及兩個(gè)著名的函數(shù),即setTimeout與setInterval。遺憾的是,這兩個(gè)著名的計(jì)時(shí)器函數(shù)都有自己的一些缺陷。正如我們在1.1.2節(jié)中看到的,其中有個(gè)缺陷是無法彌補(bǔ)的:當(dāng)同一個(gè)JavaScript進(jìn)程正運(yùn)行著代碼時(shí),任何JavaScript計(jì)時(shí)函數(shù)都無法使其他代碼運(yùn)行起來。但是,即便容忍了這一局限性,setTimeout及setInterval的不確定性也會令人犯怵。下面是一個(gè)示例。EventModel/fireCount.jsvarfireCount=0;varstart=newDate;vartimer=setInterval(function(){if(newDate-start>1000){clearInterval(timer);console.log(fireCount);return;}fireCount++;},0);如果使用setInterval調(diào)度事件且延遲設(shè)定為0毫秒,則會盡可能頻繁地運(yùn)行此事件,對嗎?那么,在運(yùn)行于高速英特爾i7處理器之上的現(xiàn)代瀏覽器中,此事件的觸發(fā)頻率到底如何呢?大約為200次/秒。這是Chrome、Safari和Firefox等瀏覽器的平均值。在Node環(huán)境下,此事件的觸發(fā)頻率大約能達(dá)到1000次/秒。(若使用setTimeout來調(diào)度事件,重復(fù)這些實(shí)驗(yàn)也會得到類似的結(jié)果。)作為對比,如果將setInterval替換成簡單的while循環(huán),則在Chrome中此事件的觸發(fā)頻率將達(dá)到400萬次/秒,而在Node中會達(dá)到500萬次/秒!(1.2異步函數(shù)的類型|9這是怎么回事?最后我們發(fā)現(xiàn),setTimeout和setInterval就是想設(shè)計(jì)成慢吞吞的!事實(shí)上,HTML規(guī)范(這是所有主要瀏覽器都遵守的規(guī)范)推行的延時(shí)/時(shí)隔的最小值就是4毫秒?、倌敲?,如果需要更細(xì)粒度的計(jì)時(shí),該怎么辦呢?有些運(yùn)行時(shí)環(huán)境提供了備選方案。在Node中,process.nextTick允許將事件調(diào)度成盡可能快地觸發(fā)。對于筆者的系統(tǒng),process.nextTick事件的觸發(fā)頻率可以超過10萬次/秒。一些現(xiàn)代瀏覽器(含IE9+)帶有一個(gè)requestAnimationFrame函數(shù)。此函數(shù)有兩個(gè)目標(biāo):一方面,它允許以60+幀/秒的速度運(yùn)行JavaScript動畫;另一方面,它又避免后臺選項(xiàng)卡運(yùn)行這些動畫,從而節(jié)約CPU周期。在最新版的Chrome瀏覽器中,甚至能實(shí)現(xiàn)亞毫秒級的精度。②盡管這些計(jì)時(shí)函數(shù)是異步JavaScript混飯吃的家伙什兒,但永遠(yuǎn)不要忘記,setTimeout和setInterval就是些不精確的計(jì)時(shí)工具。在Node中,如果只是想產(chǎn)生一個(gè)短時(shí)延遲,請使用process.nextTick。在瀏覽器端,請嘗試使用墊片技術(shù)(shim):在支持③的瀏覽器中,推薦使用requestAnimationFramerequestAnimationFrame;在不支持requestAnimationFrame的——————————①參見http://./specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout。②參見http://updates.html5rocks./2012/05/requestAnimationFrame-API-now-with-sub-millisecond-precision。③http://paulirish./2011/requestanimationframe-for-smart-animating/。墊片技術(shù)可簡述為,它負(fù)責(zé)將一個(gè)新的API引入到一個(gè)舊的環(huán)境中,且僅僅依靠舊環(huán)境中已有的手段來實(shí)現(xiàn)?!g者注(

10|第1章深入理解JavaScript事件瀏覽器中,則退而使用setTimeout。到這里,關(guān)于JavaScript基本異步函數(shù)的簡要概覽就結(jié)束了。但怎樣才能知道一個(gè)函數(shù)到底何時(shí)異步呢?下一節(jié)中,我們在親自編寫異步函數(shù)的同時(shí)再思考這個(gè)問題。1.3異步函數(shù)的編寫JavaScript中的每個(gè)異步函數(shù)都構(gòu)建在其他某個(gè)或某些異步函數(shù)之上。凡是異步函數(shù),從上到下(一直到原生代碼)都是異步的!反之亦然:任何函數(shù)只要使用了異步的函數(shù),就必須以異步的方式給出其操作結(jié)果。正如我們在1.1.2節(jié)學(xué)到的,JavaScript并沒有提供一種機(jī)制以阻止函數(shù)在其異步操作結(jié)束之前返回。事實(shí)上,除非函數(shù)返回,否則不會觸發(fā)任何異步事件。本節(jié)將考察異步函數(shù)設(shè)計(jì)的一些常見模式。我們將看到有些函數(shù)如反復(fù)無常的小人,非得等到特定時(shí)候才下決心成為異步的。不過,我們先來精確地定義異步函數(shù)。1.3.1何時(shí)稱函數(shù)為異步的異步函數(shù)這個(gè)術(shù)語有點(diǎn)名不副實(shí):調(diào)用一個(gè)函數(shù)時(shí),程序只在該函數(shù)返回之后才能繼續(xù)。JavaScript寫手如果稱一個(gè)函數(shù)為“異步的”,其意思是這個(gè)函數(shù)會導(dǎo)致將來再運(yùn)行另一個(gè)函數(shù),后者取自于事件隊(duì)列(若后面這個(gè)函數(shù)是作為參數(shù)傳遞給前者的,則稱其為回調(diào)函數(shù),簡稱為回調(diào))。于是,一個(gè)取用回調(diào)的異步函數(shù)永遠(yuǎn)都能通過以下測試。(

1.3異步函數(shù)的編寫|11varfunctionHasReturned=false;asyncFunction(function(){console.assert(functionHasReturned);});functionHasReturned=true;異步函數(shù)還涉及另一個(gè)術(shù)語,即非阻塞。非阻塞這個(gè)詞強(qiáng)調(diào)了異步函數(shù)的高速度:異步MySQL數(shù)據(jù)庫驅(qū)動程序做一個(gè)查詢可能要花上一小時(shí),但負(fù)責(zé)發(fā)送查詢請求的那個(gè)函數(shù)卻能以微秒級速度返回。這對于那些需要快速處理海量請求的網(wǎng)站服務(wù)器來說,絕對是個(gè)福音。通常,那些取用回調(diào)的函數(shù)都會將其作為自己的最后一個(gè)參數(shù)。(可惜的是,老資格的setTimeout和setInterval都是這一約定的特例。)不過,有些異步函數(shù)也會間接取用回調(diào),它們會返回Promise對象或使用PubSub模式。本書稍后就會介紹這些異步設(shè)計(jì)模式。遺憾的是,要想確認(rèn)某個(gè)函數(shù)異步與否,唯一的方法就是審查其源代碼。有些同步函數(shù)卻擁有看起來像是異步的API,這或者是因?yàn)樗鼈儗砜赡軙兂僧惒降?,又或者是因?yàn)榛卣{(diào)這種形式能方便地返回多個(gè)參數(shù)。一旦存疑,請別指望函數(shù)就是異步的。1.3.2間或異步的函數(shù)有些函數(shù)某些時(shí)候是異步的,但其他時(shí)候卻不然。舉個(gè)例子,jQuery的同名函數(shù)(通常記作$)可用于延遲函數(shù)直至DOM已經(jīng)結(jié)束加載。但是,若DOM早已結(jié)束了加載,則不存在任何延遲,$的回調(diào)將會立即觸發(fā)。不注意的話,這種行為的不可預(yù)知性會帶來很多麻煩。我曾經(jīng)看到也犯過這樣一個(gè)錯(cuò)誤,即假定$會在已加載本頁面其他腳本之后再運(yùn)行(

12|第1章深入理解JavaScript事件一個(gè)函數(shù)。//application.js$(function(){utils.log('Ready');});//utils.jswindow.utils={log:function(){if(window.console)console.log.apply(console,arguments);}};<scriptsrc="application.js"></script><scriptsrc="util.js"></script>這段代碼運(yùn)行得很好,但前提是瀏覽器并未從緩存中加載頁面(這會導(dǎo)致DOM早在腳本運(yùn)行之前就已加載就緒)。如果出現(xiàn)這種情況,傳遞給$的回調(diào)就會在設(shè)置utils.log之前運(yùn)行,從而導(dǎo)致一個(gè)錯(cuò)誤。(為了避免這種情況,應(yīng)該采用一種更現(xiàn)代的管理客戶端依賴性的方法。請參閱第6章。)下面來看另一個(gè)例子。1.3.3緩存型異步函數(shù)間或異步的函數(shù)有一個(gè)常見變種是可緩存結(jié)果的異步請求類函數(shù)。舉例來說,假設(shè)正在編寫一個(gè)基于瀏覽器的計(jì)算器,它使用了網(wǎng)頁Worker對象以單獨(dú)開一個(gè)線程來進(jìn)行計(jì)算。(第5章將介紹網(wǎng)頁Worker對象的API。)主腳本看起來像這樣:①——————————①訪問http://webworkersandbox./5009efc12245588e410002cf,可以看到這個(gè)例子的一個(gè)可運(yùn)行版本。(

1.3異步函數(shù)的編寫|13varcalculationCache={},calculationCallbacks={},mathWorker=newWorker('calculator.js');mathWorker.addEventListener('message',function(e){varmessage=e.data;calculationCache[message.formula]=message.result;calculationCallbacks[message.formula](message.result);});functionrunCalculation(formula,callback){if(formulaincalculationCache){returncallback(calculationCache[formula]);};if(formulaincalculationCallbacks){returnsetTimeout(function(){runCalculation(formula,callback);},0);};mathWorker.postMessage(formula);calculationCallbacks[formula]=callback;}在這里,當(dāng)結(jié)果已經(jīng)緩存時(shí),runCalculation函數(shù)是同步的,否則就是異步的。存在3種可能的情景。公式已經(jīng)計(jì)算完成,于是結(jié)果位于calculationCache中。這種情況下,runCalculation是同步的。公式已經(jīng)發(fā)送給Worker對象,但尚未收到結(jié)果。這種情況下,runCalculation設(shè)定了一個(gè)延時(shí)以便再次調(diào)用自身;重復(fù)這一過程直到結(jié)果位于calculationCache中為止。公式尚未發(fā)送給Worker對象。這種情況下,將會從Worker對象的'message'事件監(jiān)聽器激活回調(diào)。(

14|第1章深入理解JavaScript事件請注意,在第2種和第3種情景中,我們按照兩種不同的方式來等待任務(wù)的完成。這個(gè)例子寫成這樣,就是為了演示依據(jù)哪幾種常見方式來等待某些東西發(fā)生改變(如緩存型計(jì)算公式的值)。是不是應(yīng)該傾向于其中某種方式呢?我們接著往下看。1.3.4異步遞歸與回調(diào)存儲在runCalculation函數(shù)中,為了等待Worker對象完成自己的工作,或者通過延時(shí)而重復(fù)相同的函數(shù)調(diào)用(即異步遞歸),或者簡單地存儲回調(diào)結(jié)果。哪種方式更好呢?乍一看,只使用異步遞歸是最簡單的,因?yàn)檫@里不再需要calculationCallbacks對象。出于這個(gè)目的,JavaScript新手常常會使用setTimeout,因?yàn)樗芟窬€程型語言的風(fēng)格。此程序的Java版本可能會有這樣一個(gè)循環(huán):while(!calculationCache.get(formula)){Thread.sleep(0);};但是,延時(shí)并不是免費(fèi)的午餐。大量延時(shí)的話,會造成巨大的計(jì)算荷載。異步遞歸有一點(diǎn)很可怕,即在等待任務(wù)完成期間,可觸發(fā)之延時(shí)的次數(shù)是不受限的!此外,異步遞歸還毫無必要地復(fù)雜化了應(yīng)用程序的事件結(jié)構(gòu)?;谶@些原因,應(yīng)將異步遞歸視作一種“反模式”的方式。在這個(gè)計(jì)算器例子中,為了避免異步遞歸,可以為每個(gè)公式存儲一個(gè)回調(diào)數(shù)組。(

1.3異步函數(shù)的編寫|15varcalculationCache={},calculationCallbacks={},mathWorker=newWorker('calculator.js');mathWorker.addEventListener('message',function(e){varmessage=e.data;calculationCache[message.formula]=message.result;calculationCallbacks[message.formula].forEach(function(callback){callback(message.result);});});functionrunCalculation(formula,callback){if(formulaincalculationCache){returncallback(calculationCache[formula]);};if(formulaincalculationCallbacks){returncalculationCallbacks[formula].push(callback);};mathWorker.postMessage(formula);calculationCallbacks[formula]=[callback];}沒有了延時(shí),我們的代碼要直觀得多,也高效得多。總的來說,請避免異步遞歸。僅當(dāng)所采用的庫提供了異步功能但沒有提供任何形式的回調(diào)機(jī)制時(shí),異步遞歸才有必要。如果真的遇到這種情況,要做的第一件事應(yīng)該是為該庫寫一個(gè)補(bǔ)丁?;蛘?,干脆找一個(gè)更好的庫。1.3.5返值與回調(diào)的混搭在以上兩種runCalculation實(shí)現(xiàn)中,有時(shí)會用到返值技術(shù)。這是出于簡潔的目的而隨意作出的選擇。下面這行代碼(

16|第1章深入理解JavaScript事件returncallback(calculationCache[formula]);很容易即可改寫成callback(calculationCache[formula]);return;這是因?yàn)椴]有打算使用這個(gè)返值。這是JavaScript的一種普遍做法,而且通常無害。不過,有些函數(shù)既返回有用的值,又要取用回調(diào)。這類情況下,切記回調(diào)有可能被同步調(diào)用(返值之前),也有可能被異步調(diào)用(返值之后)。永遠(yuǎn)不要定義一個(gè)潛在同步而返值卻有可能用于回調(diào)的函數(shù)。舉個(gè)例子,下面這個(gè)負(fù)責(zé)打開WebSocket連接以連至給定服務(wù)器的函數(shù)(使①用緩存技術(shù)以確保每個(gè)服務(wù)器只有一個(gè)連接)就違反了上述規(guī)則。varwebSocketCache={};functionopenWebSocket(serverAddress,callback){varsocket;if(serverAddressinwebSocketCache){socket=webSocketCache[serverAddress];if(socket.readyState===WebSocket.OPEN){callback();}else{socket.onopen=_.pose(callback,socket.onopen);};}else{socket=newWebSocket(serverAddress);webSocketCache[serverAddress]=socket;——————————①參見/en/WebSockets/。(

1.3異步函數(shù)的編寫|17socket.onopen=callback;};returnsocket;};(這段代碼依賴于Underscore.js庫。_.pose定義的這個(gè)新函數(shù)既運(yùn)行了callback,又運(yùn)行了初始的socket.onopen回調(diào)。①)這段代碼的問題在于,如果套接字已經(jīng)緩存且打開,則會在函數(shù)返值之前就運(yùn)行回調(diào),這會使以下代碼崩潰。varsocket=openWebSocket(url,function(){socket.send('Hello,server!');});怎么解決呢?將回調(diào)封裝在setTimeout中即可。if(socket.readyState===WebSocket.OPEN){setTimeout(callback,0);}else{//...}這里使用延時(shí)會讓人感覺是在東拼西湊,但這總比API自相矛盾要好得多。在本節(jié)中,我們看到了一些編寫異步函數(shù)的最佳實(shí)踐。請勿依賴那些看似始終異步的函數(shù),除非已經(jīng)閱讀其源代碼。請避免使用計(jì)時(shí)器方法來等待某個(gè)會變化的東西。如果同一個(gè)函數(shù)既返值又運(yùn)行回調(diào),則請確?;卣{(diào)在返值之后才運(yùn)行。一次消化這些信息確實(shí)太多了一點(diǎn),不過,編寫好的異步函數(shù)確實(shí)是——————————①參見http://documentcloud.github./underscore/#pose。(

18|第1章深入理解JavaScript事件寫出優(yōu)秀JavaScript代碼的關(guān)鍵所在。1.4異步錯(cuò)誤的處理像很多時(shí)髦的語言一樣,JavaScript也允許拋出異常,隨后再用一個(gè)try/catch語句塊捕獲。如果拋出的異常未被捕獲,大多數(shù)JavaScript環(huán)境都會提供一個(gè)有用的堆棧軌跡。舉個(gè)例子,下面這段代碼由于'{'為無效JSON對象而拋出異常。EventModel/stackTrace.jsfunctionJSONToObject(jsonStr){returnJSON.parse(jsonStr);}varobj=JSONToObject('{');SyntaxError:UnexpectedendofinputatObject.parse(native)atJSONToObject(/AsyncJS/stackTrace.js:2:15)atObject.<anonymous>(/AsyncJS/stackTrace.js:4:11)堆棧軌跡不僅告訴我們哪里拋出了錯(cuò)誤,而且說明了最初出錯(cuò)的地方:第4行代碼。遺憾的是,自頂向下地跟蹤異步錯(cuò)誤起源并不都這么直截了當(dāng)。在本節(jié)中,我們會看到為什么throw很少用作回調(diào)內(nèi)錯(cuò)誤處理的正確工具,還會了解如何設(shè)計(jì)異步API以繞開這一局限。1.4.1回調(diào)內(nèi)拋出的錯(cuò)誤如果從異步回調(diào)中拋出錯(cuò)誤,會發(fā)生什么事?讓我們先來做個(gè)測試。EventModel/nestedErrors.jssetTimeout(functionA(){setTimeout(functionB(){(1.4異步錯(cuò)誤的處理|19setTimeout(functionC(){thrownewError('Somethingterriblehashappened!');},0);},0);},0);上述應(yīng)用的結(jié)果是一條極其簡短的堆棧軌跡。Error:Somethingterriblehashappened!atTimer.C(/AsyncJS/nestedErrors.js:4:13)等等,A和B發(fā)生了什么事?為什么它們沒有出現(xiàn)在堆棧軌跡中?這是因?yàn)檫\(yùn)行C的時(shí)候,A和B并不在內(nèi)存堆棧里。這3個(gè)函數(shù)都是從事件隊(duì)列直接運(yùn)行的?;谕瑯拥睦碛?,利用try/catch語句塊并不能捕獲從異步回調(diào)中拋出的錯(cuò)誤。下面進(jìn)行演示。EventModel/asyncTry.jstry{setTimeout(function(){thrownewError('Catchmeifyoucan!');},0);}catch(e){console.error(e);}看到這里的問題了嗎?這里的try/catch語句塊只捕獲setTimeout函數(shù)自身內(nèi)部發(fā)生的那些錯(cuò)誤。因?yàn)閟etTimeout異步地運(yùn)行其回調(diào),所以即使延時(shí)設(shè)置為0,回調(diào)拋出的錯(cuò)誤也會直接流向應(yīng)用程序的未捕獲異常處理器(請參閱1.4.2節(jié))??偟膩碚f,取用異步回調(diào)的函數(shù)即使包裝上try/catch語句塊,也只是無用之舉。(特例是,該異步函數(shù)確實(shí)是在同步地做某些事且容(20|第1章深入理解JavaScript事件易出錯(cuò)。例如,Node的fs.watch(file,callback)就是這樣一個(gè)函數(shù),它在目標(biāo)文件不存在時(shí)會拋出一個(gè)錯(cuò)誤。)正因?yàn)榇?,Node.js中的回調(diào)幾乎總是接受一個(gè)錯(cuò)誤作為其首個(gè)參數(shù),這樣就允許回調(diào)自己來決定如何處理這個(gè)錯(cuò)誤。舉個(gè)例子,下面這個(gè)Node應(yīng)用嘗試異步地讀取一個(gè)文件,還負(fù)責(zé)記錄下任何錯(cuò)誤(如“文件不存在”)。EventModel/readFile.jsvarfs=require('fs');fs.readFile('fhgwgdz.txt',function(err,data){if(err){returnconsole.error(err);};console.log(data.toString('utf8'));});客戶端JavaScript庫的一致性要稍微差些,不過最常見的模式是,針對成敗這兩種情形各規(guī)定一個(gè)單獨(dú)的回調(diào)。jQuery的Ajax方法就遵循了這個(gè)模式。$.get('/data',{success:successHandler,failure:failureHandler});不管API形態(tài)像什么,始終要記住的是,只能在回調(diào)內(nèi)部處理源于回調(diào)的異步錯(cuò)誤。異步尤達(dá)大師會說:“做,或者不做,沒有試試看一說?!?.4.2未捕獲異常的處理如果是從回調(diào)中拋出異常的,則由那個(gè)調(diào)用了回調(diào)的人負(fù)責(zé)捕獲該異常。但如果異常從未被捕獲,又會怎么樣?這時(shí),不同的JavaScript(1.4異步錯(cuò)誤的處理|21環(huán)境有著不同的游戲規(guī)則……1.在瀏覽器環(huán)境中現(xiàn)代瀏覽器會在開發(fā)人員控制臺顯示那些未捕獲的異常,接著返回事件隊(duì)列。要想修改這種行為,可以給window.onerror附加一個(gè)處理器。如果windows.onerror處理器返回true,則能阻止瀏覽器的默認(rèn)錯(cuò)誤處理行為。window.onerror=function(err){returntrue;//徹底忽略所有錯(cuò)誤};在成品應(yīng)用中,會考慮某種JavaScript錯(cuò)誤處理服務(wù),譬如Errorception。Errorception提供了一個(gè)現(xiàn)成的windows.onerror處①理器,它向應(yīng)用服務(wù)器報(bào)告所有未捕獲的異常,接著應(yīng)用服務(wù)器發(fā)送消息通知我們。2.在Node.js環(huán)境中在Node環(huán)境中,window.onerror的類似物就是process對象的uncaughtException事件。正常情況下,Node應(yīng)用會因未捕獲的異常而立即退出。但只要至少還有一個(gè)uncaughtException事件處理器,Node應(yīng)用就會直接返回事件隊(duì)列。process.on('uncaughtException',function(err){console.error(err);//避免了關(guān)停的命運(yùn)!});但是,自Node0.8.4起,uncaughtException事件就被廢棄了。據(jù)——————————①參見http://errorception./。(

22|第1章深入理解JavaScript事件其文檔所言,①對異常處理而言,uncaughtException是一種非常粗暴的機(jī)制,它在將來可能會被放棄……請勿使用uncaughtException,而應(yīng)使用Domain對象。Domain對象又是什么?你可能會這樣問。Domain對象是事件化對象(第2章會詳細(xì)討論),它將throw轉(zhuǎn)化為'error'事件。下面是一個(gè)例子。EventModel/domainThrow.jsvarmyDomain=require('domain').create();myDomain.run(function(){setTimeout(function(){thrownewError('Listentome!')},50);});myDomain.on('error',function(err){console.log('Errorignored!');});源于延時(shí)事件的throw只是簡單地觸發(fā)了Domain對象的錯(cuò)誤處理器。Errorignored!很奇妙,是不是?Domain對象讓throw語句生動了很多。遺憾的是,僅在Node0.8+環(huán)境中才能使用Domain對象;在我寫作本書時(shí),Domain對象仍被視作試驗(yàn)性的特性。更多信息請參閱Node文檔。②不管在瀏覽器端還是服務(wù)器端,全局的異常處理器都應(yīng)被視作最后一——————————①參見/docs/latest/api/process.html#process_event_uncaughtexception。②參見/docs/latest/api/domain.html。(1.4異步錯(cuò)誤的處理|23根救命稻草。請僅在調(diào)試時(shí)才使用它。1.4.3拋出還是不拋出遇到錯(cuò)誤時(shí),最簡單的解決方法就是拋出這個(gè)錯(cuò)誤。在Node代碼中,大家會經(jīng)??吹筋愃七@樣的回調(diào):function(err){if(err)throwerr;//...}在第4章中,我們會經(jīng)常沿用這一做法。但是,在成品應(yīng)用中,允許例行的異常及致命的錯(cuò)誤像踢皮球一樣踢給全局處理器,這是不可接受的?;卣{(diào)中的throw相當(dāng)于JavaScript寫手在說“現(xiàn)在我還不想考慮這個(gè)”。如果拋出那些自己知道肯定會被捕獲的異常呢?這種做法同樣兇險(xiǎn)萬分。2011年,IsaacSchlueter(npm的開發(fā)者,在任的Node開發(fā)負(fù)責(zé)人)就主張try/catch是一種“反模式”的方式。①try/catch只是包裝著漂亮花括弧的goto語句。一旦跑去處理錯(cuò)誤,就無法回到中斷之處繼續(xù)向下執(zhí)行。更糟糕的是,通過throw語句的代碼,完全不知道自己會跳到什么地方。返回錯(cuò)誤碼的時(shí)候,就相當(dāng)于正在履行合約。拋出錯(cuò)誤的時(shí)候,就好像在說,“我知道我正在和你說話,但我現(xiàn)在不想搭理你,我要先找你老板談?wù)劇?,這太粗俗無禮了。如果不是什么緊急情況,請別這么做;如果確實(shí)是緊急情況,則應(yīng)該直接崩潰掉?!賲⒁奾ttps://groups.google./forum/#!topic/nodejs/1ESsssIxrUU。(

24|第1章深入理解JavaScript事件Schlueter提倡完全將throw用作斷言似的構(gòu)造結(jié)構(gòu),作為一種掛起應(yīng)用的方式——當(dāng)應(yīng)用在做完全沒預(yù)料到的事時(shí),即掛起應(yīng)用。Node社區(qū)主要遵循這一建議,盡管這種情況可能會隨著Domain對象的出現(xiàn)而改變。那么,關(guān)于異步錯(cuò)誤的處理,目前的最佳實(shí)踐是什么呢?我認(rèn)為應(yīng)該聽從Schlueter的建議:如果想讓整個(gè)應(yīng)用停止工作,請勇往直前地大膽使用throw。否則,請認(rèn)真考慮一下應(yīng)該如何處理錯(cuò)誤。是想給用戶顯示一條出錯(cuò)消息嗎?是想重試請求嗎?還是想唱一曲“雛菊鈴之歌”?那就這么處理吧,只是請盡可能地靠近錯(cuò)誤源頭。①1.5嵌套式回調(diào)的解嵌套JavaScript中最常見的反模式做法是,回調(diào)內(nèi)部再嵌套回調(diào)。還記得前言里提到的金字塔厄運(yùn)嗎?我們先來看一個(gè)具體的例子,你也可能在Node服務(wù)器上看到過類似的代碼。functioncheckPassword(username,passwordGuess,callback){varqueryStr='SELECT*FROMuserWHEREusername=?';db.query(queryStr,username,function(err,result){if(err)throwerr;hash(passwordGuess,function(passwordGuessHash){callback(passwordGuessHash===result['password_hash']);});});}這里定義了一個(gè)異步函數(shù)checkPassword,它觸發(fā)了另一個(gè)異步函數(shù)db.query,而后者又可能觸發(fā)另外一個(gè)異步函數(shù)hash。(在——————————①1892年著名的歌曲DaisyBell,這是第一首由電腦模擬人聲唱出的歌曲?!g者注(

1.5嵌套式回調(diào)的解嵌套|25閱讀代碼之前,無法確認(rèn)這些函數(shù)是否真的異步,但這里的幾個(gè)函數(shù)理應(yīng)如此。)這段代碼有什么問題呢?目前為止,沒有任何問題。它能用,而且簡潔明了。但是,如果試圖向其添加新特性,它就會變得毛里毛躁、險(xiǎn)象環(huán)生,比如去處理那個(gè)數(shù)據(jù)庫錯(cuò)誤,而不是拋出錯(cuò)誤(請參閱1.4.3節(jié))、記錄嘗試訪問數(shù)據(jù)庫的次數(shù)、阻塞訪問數(shù)據(jù)庫,等等。嵌套式回調(diào)誘惑我們通過添加更多代碼來添加更多特性,而不是將這些特性實(shí)現(xiàn)為可管理、可重用的代碼片段。checkPassword有一種可以避免出現(xiàn)上述苗頭的等價(jià)實(shí)現(xiàn)方式,如下:functioncheckPassword(username,passwordGuess,callback){varpasswordHash;varqueryStr='SELECT*FROMuserWHEREusername=?';db.query(qyeryStr,username,queryCallback);functionqueryCallback(err,result){if(err)throwerr;passwordHash=result['password_hash'];hash(passwordGuess,hashCallback);}functionhashCallback(passwordGuessHash){callback(passwordHash===passwordGuessHash);}}這種寫法更啰嗦一些,但讀起來更清晰,也更容易擴(kuò)展。由于這里賦予了異步結(jié)果(即passwordHash)更寬廣的作用域,所以獲得了更大的靈活性。按照慣例,請避免兩層以上的函數(shù)嵌套。關(guān)鍵是找到一種在激活異步(

26|第1章深入理解JavaScript事件調(diào)用之函數(shù)的外部存儲異步結(jié)果的方式,這樣回調(diào)本身就沒有必要再嵌套了。如果這樣聽起來有點(diǎn)詰聱難懂,請別擔(dān)心。我們在后續(xù)幾章中會看到大量的異步事件例子,那里的異步事件順序運(yùn)行且沒有嵌套式事件處理器。1.6小結(jié)本章闡釋了JavaScript的單線程性為什么既是福利又是禍害。使用得當(dāng)?shù)脑?,它會使代碼優(yōu)美且沒有那些多線程應(yīng)用中泛濫成災(zāi)的可怕競態(tài)條件。不過,這需要你形成正確的思維定勢并掌握恰當(dāng)?shù)募夹g(shù)。本書其余章節(jié)將介紹JavaScript中處理事件時(shí)用到的一些庫和設(shè)計(jì)模式。我們考查的所有示例都可以運(yùn)行于主流的瀏覽器或未經(jīng)改動的Node.js環(huán)境。不過,編寫JavaScript并不是產(chǎn)生JavaScript代碼的唯一途徑。關(guān)于其他一些有趣編輯器的概況,請參閱附錄A。這里值得提一下,JavaScript中存在一種多線程性:可以孵化出Worker進(jìn)程。每個(gè)孵化出的進(jìn)程都可以與其他進(jìn)程交換數(shù)據(jù),其限制等同于任何其他I/O進(jìn)程。Worker對象使得我們有可能利用多個(gè)內(nèi)核,同時(shí)不會破壞JavaScript的游戲規(guī)則(代碼不可能被中斷;變量只有處于其作用域內(nèi)部時(shí)才是可訪問的)。關(guān)于Worker對象的更多內(nèi)容,請參見第5章。接下來兩章將專門討論兩種基本的設(shè)計(jì)模式。PubSub模式是一種將回調(diào)賦值給已命名事件的回調(diào)組織方式,而Promise對象是一種表示一次性事件的直觀對象。(

2.1PubSub模式|27第2章分布式事件在上一章中,我們了解了JavaScript異步事件的工作方式。但在實(shí)踐中到底應(yīng)該怎樣處理這些事件呢?這個(gè)問題聽起來好像很愚蠢。直接給應(yīng)用程序關(guān)心的每個(gè)事件都附加一個(gè)處理器不就行了嗎?然而,一旦單一的事件有著多重的后果,這種“一事一處理”的方式將迫使處理器規(guī)模急劇膨脹。假設(shè)我們正在構(gòu)建一個(gè)類似于GoogleDocs的網(wǎng)頁版文字處理程序。每當(dāng)用戶按下一個(gè)鍵時(shí),都要做很多事情:新鍵入的字符必須顯示在屏幕上;插入點(diǎn)必須向后移動;這次鍵入動作必須推入本地的撤銷動作歷史記錄中,且必須與服務(wù)器進(jìn)行同步;拼寫檢查功能也必須運(yùn)行起來;字?jǐn)?shù)統(tǒng)計(jì)和頁數(shù)統(tǒng)計(jì)也需要加以更新。用一個(gè)keypress處理器就想完成所有這些任務(wù)甚至更多任務(wù),這顯然會令人望而卻步。從純機(jī)械論的角度看,每項(xiàng)因響應(yīng)事件而執(zhí)行的任務(wù)都確實(shí)必須由事件處理器發(fā)起。但是,從人類感性的角度出發(fā),這個(gè)龐大的事件處理器通常最好能替換成更具延展性的、動態(tài)的構(gòu)造——一種可以在運(yùn)行時(shí)對其增減任務(wù)的構(gòu)造。簡而言之,我們希望使用分布式事件:事件的蝴蝶偶然扇動了下翅膀,整個(gè)應(yīng)用到處都引發(fā)了反應(yīng)。(28|第2章分布式事件在本章中,你將會學(xué)到如何使用PubSub(Publish/Subscribe,意為“發(fā)布/訂閱”)模式來分發(fā)事件。沿著這個(gè)思路,我們會看到PubSub模式的一些具體表現(xiàn):Node的EventEmitter對象、Backbone的事件化模型和jQuery的自定義事件。在這些工具的幫助下,我們能解嵌套那些嵌套式回調(diào),減少重復(fù)冗余,最終編寫出易于理解的事件驅(qū)動型代碼。2.1PubSub模式從JavaScript誕生之日起,瀏覽器就允許向DOM元素附加事件處理器,形如:link.onclick=clickHandler;啊哈,一目了然!只不過要提醒你一點(diǎn):如果想向一個(gè)元素附加兩個(gè)點(diǎn)擊事件處理器,則必須自行用一個(gè)封裝函數(shù)匯集這兩個(gè)處理器。link.onclick=function(){clickHandler1.apply(this,arguments);clickHandler2.apply(this,arguments);};這不僅冗長重復(fù),而且也會制造出浮腫的、“全能的”處理器函數(shù)。正因?yàn)榇?,W3C于2000年向DOM規(guī)范中添加了addEventListener方法,而jQuery將其抽象成bind方法。使用bind,很容易對任何元素或元素集合發(fā)生的任何事件添加任意多的處理器,且完全不用擔(dān)心這些處理器因摩肩接踵而出現(xiàn)踩踏事故。$(link).bind('click',clickHandler1)(

2.1PubSub模式|29.bind('click',clickHandler2);(在jQuery1.7+中,優(yōu)先使用新的on語法而不用bind。那里也提供①了click方法,不過它只是bind('click',...)的簡寫。但是,筆者傾向于一直使用bind/on。)從軟件架構(gòu)的角度看,jQuery將link元素的事件發(fā)布給了任何想訂閱此事件的人。這正是稱其為PubSub模式的原因。在老式DOM的事件API中,綁定至事件意味著要編寫object.onevent=...這樣的代碼,但現(xiàn)在它差不多被人忘光了,人們都轉(zhuǎn)投至PubSub的懷抱了。Node的API架構(gòu)師因?yàn)樘矚gPubSub,所以決定包含一個(gè)一般性的PubSub實(shí)體。這個(gè)實(shí)體叫做EventEmitter(事件發(fā)生器),其他對象可以繼承它。Node中幾乎所有的I/O源都是EventEmitter對象:文件流、HTTP服務(wù)器,甚至是應(yīng)用進(jìn)程本身。以下例為證。Distributed/processExit.js['room','moon','cowjumpingoverthemoon'].forEach(function(name){process.on('exit',function(){console.log('Goodnight,'+name);});});瀏覽器端存在著無數(shù)的單機(jī)版PubSub庫。此外,很多MVC框架,如Backbone.js和Spine,都提供了自己的類EventEmitter模塊。本章稍后再更詳細(xì)地討論Backbone?!賲⒁奾ttp://api.jquery./on/。(30|第2章分布式事件2.1.1EventEmitter對象我們用Node的EventEmitter對象作為PubSub接口的例子。EventEmitter有著簡單而近乎最簡化的設(shè)計(jì)。要想給EventEmitter對象添加一個(gè)事件處理器,只要以事件類型和事件處理器為參數(shù)調(diào)用on方法即可。emitter.on('evacuate',function(message){console.log(message);});emit(意為“觸發(fā)”)方法負(fù)責(zé)調(diào)用給定事件類型的所有處理器。舉個(gè)例子,下面這行代碼:emitter.emit('evacuate');將調(diào)用evacuate事件的所有處理器。請注意,這里的術(shù)語事件跟事件隊(duì)列沒有任何關(guān)系。請參閱2.1.3節(jié)。使用emit方法觸發(fā)事件時(shí),可以添加任意多的附加參數(shù)。所有參數(shù)均傳遞至所有處理器。emitter.emit('evacuate','Womanandchildrenfirst!');事件名稱不存在任何限制,然而Node相關(guān)文檔還是規(guī)定了一條有用的約定。通常,事件名稱會表示為一個(gè)駝峰式大小寫混合的字符串。①——————————①參見/docs/latest/api/events.html。駝峰式大小寫是一種命名慣例,目的是增強(qiáng)標(biāo)識符的可辨識度和可讀性,因?qū)懙酶叩湾e(cuò)落如駝峰而得名,可以分成camelCased、CamelCased等多種形式。——譯者注(

2.1PubSub模式|31EventEmitter對象的所有方法都是公有的,但一般約定只能從EventEmitter對象的“內(nèi)部”觸發(fā)事件。也就是說,如果有一個(gè)對象繼承了EventEmitter原型并使用了this.emit方法來廣播事件,則不應(yīng)該從這個(gè)對象之外的其他地方再調(diào)用其emit方法。2.1.2玩轉(zhuǎn)自己的PubSubPubSub模式的實(shí)現(xiàn)如此簡單,以至于用十幾行代碼就能建立自己的PubSub實(shí)現(xiàn)。對于支持的每種事件類型,唯一需要存儲的狀態(tài)值就是一個(gè)事件處理器清單。PubSub={handlers:{}}需要添加事件監(jiān)聽器時(shí),只要將監(jiān)聽器推入數(shù)組末尾即可(這意味著總是會按照添加監(jiān)聽器的次序來調(diào)用監(jiān)聽器)。PubSub.on=function(eventType,handler){if(!(eventTypeinthis.handlers)){this.handlers[eventType]=[];}this.handlers[eventType].push(handler);returnthis;}接著,等到觸發(fā)事件的時(shí)候,再循環(huán)遍歷所有的事件處理器。PubSub.emit=function(eventType){varhandlerArgs=Atotype.slice.call(arguments,1);for(vari=0;i<this.handlers[eventType].length;i++){this.handlers[eventType][i].apply(this,handlerArgs);}returnthis;}(

32|第2章分布式事件就是這么簡單!現(xiàn)在只實(shí)現(xiàn)了Node之EventEmitter對象的核心部分。(還沒實(shí)現(xiàn)的重要部分只剩下移除事件處理器及附加一次性事件處理器等功能。)當(dāng)然,各種PubSub實(shí)現(xiàn)在特性方面會稍有不同。jQuery團(tuán)隊(duì)注意到j(luò)Query庫里到處都在用幾個(gè)不同的PubSub實(shí)現(xiàn),于是決定在jQuery1.7中將它們抽象為$.Callbacks。這樣就不再用數(shù)組來存儲各種事①件類型對應(yīng)的事件處理器,而可以轉(zhuǎn)用$.Callbacks實(shí)例。很多PubSub實(shí)現(xiàn)負(fù)責(zé)解析事件字符串以提供一些特殊功能。舉個(gè)例子,你也許熟悉jQuery的名稱空間化事件:如果綁定了名稱為"click.tbb"和"hover.tbb"的兩個(gè)事件,則簡單地調(diào)用unbind(".tbb")就可以同時(shí)解綁定它們。Backbone.js允許向"all"事件類型綁定事件處理器,這樣不管發(fā)生什么事,都會導(dǎo)致這些事件處理器的觸發(fā)。jQuery和Backbone.js都支持用空格隔開多個(gè)事件來同時(shí)綁定或觸發(fā)多種事件類型,譬如"keypressmousemove"。2.1.3同步性盡管PubSub模式是一項(xiàng)處理異步事件的重要技術(shù),但它內(nèi)在跟異步?jīng)]有任何關(guān)系。請考慮下面這段代碼:$('input[type=submit]').on('click',function(){console.log('foo');}).trigger('click');console.log('bar');這段代碼的輸出為:——————————①參見http://api.jquery./jQuery.Callbacks/。(

2.1PubSub模式|33foobar這證明了click事件的處理器因trigger方法而立即被激活。事實(shí)上,只要觸發(fā)了jQuery事件,就會不被中斷地按順序執(zhí)行其所有事件處理器。好吧,我們要明確一點(diǎn):用戶點(diǎn)擊Submit(提交)按鈕時(shí),這確實(shí)是一個(gè)異步事件。點(diǎn)擊事件的第一個(gè)處理器會從事件隊(duì)列中被觸發(fā)。然而,事件處理器本身無法知道自己是從事件隊(duì)列中還是從應(yīng)用代碼中運(yùn)行的。如果事件按順序觸發(fā)了過多的處理器,就會有阻塞線程且導(dǎo)致瀏覽器不響應(yīng)的風(fēng)險(xiǎn)。更糟糕的是,如果事件處理器本身觸發(fā)了事件,還很容易造成無限循環(huán)。$('input[type=submit]').on('click',function(){$(this).trigger('click');//堆棧上溢!});回想本章開頭提到的文字處理程序的例子。用戶按鍵時(shí),需要發(fā)生很多事情,其中某些事還需要復(fù)雜的計(jì)算。全部做完這些事之后再返回事件隊(duì)列,只會制造出響應(yīng)遲鈍的應(yīng)用。這個(gè)問題有一個(gè)很好的解決方案,就是對那些無需即刻發(fā)生的事情維持一個(gè)隊(duì)列,并使用一個(gè)計(jì)時(shí)函數(shù)定時(shí)運(yùn)行此隊(duì)列中的下一項(xiàng)任務(wù)。首次嘗試編碼的結(jié)果可能像這樣:vartasks=[];setInterval(function(){varnextTask;(34|第2章分布式事件if(nextTask=tasks.shift()){nextTask();};},0);(請參閱4.4節(jié),了解一種更復(fù)雜精妙的作業(yè)排隊(duì)技術(shù)。)PubSub模式簡化了事件的命名、分發(fā)和堆積。任何時(shí)刻,只要直覺上認(rèn)為對象會聲明發(fā)生什么事情,就可以使用PubSub這種很棒的模式。2.2事件化模型只要對象帶有PubSub接口,就可以稱之為事件化對象。特殊情況出現(xiàn)在用于存儲數(shù)據(jù)的對象因內(nèi)容變化而發(fā)布事件時(shí),這里用于存儲數(shù)據(jù)的對象又稱作模型。模型就是MVC(Model-View-Controller,模型視圖控制器)中的那個(gè)M。MVC三層架構(gòu)設(shè)計(jì)模式在最近幾年里已經(jīng)成為JavaScript編程中最熱點(diǎn)的主題之一。MVC的核心理念是應(yīng)用程序應(yīng)該以數(shù)據(jù)為中心,所以模型發(fā)生的事件會影響到DOM(即MVC中的視圖)和服務(wù)器(通過MVC中的控制器而產(chǎn)生影響)。我們先來看看人氣爆棚的Backbone.js框架。可以像這樣創(chuàng)建一個(gè)新①的Model(模型)對象:style=newBackbone.Model({font:'Georgia'});model作為參數(shù)時(shí)只是代表了那個(gè)簡單的可以傳遞的JSON對象?!賲⒁奾ttp://documentcloud.github./backbone/。(

2.2事件化模型|35style.toJSON()//{"font":"Georgia"}但不同于普通對象的是,這個(gè)model對象會在發(fā)生變化時(shí)發(fā)布通知。style.on('change:font',function(model,font){alert('Thankyouforchoosing'+font+'!');});老式的JavaScript依靠輸入事件的處理器直接改變DOM。新式的JavaScript先改變模型,接著由模型觸發(fā)事件而導(dǎo)致DOM的更新。在幾乎所有的應(yīng)用程序中,這種關(guān)注層面的分離都會帶來更優(yōu)雅、更直觀的代碼。2.2.1模型事件的傳播作為最簡形式,MVC三層架構(gòu)只包括相互聯(lián)系的模型和視圖:“如果模型是這樣變化的,那么DOM就要那樣變化?!辈贿^,MVC三層架構(gòu)最大的利好出現(xiàn)在change(變化)事件冒泡上溯數(shù)據(jù)樹的時(shí)候。不用再去訂閱數(shù)據(jù)樹每片葉子上發(fā)生的事件,而只需訂閱數(shù)據(jù)樹根和枝處發(fā)生的事件即可。事件化模型的set/get方法正如我們知道的,JavaScript確實(shí)沒有一種每當(dāng)對象變化時(shí)就觸發(fā)事件的機(jī)制。因此請記住,事件化模型要想工作的話,必須要使用一些像Backbone.js之set/get這樣的方法。style.set({font:'Palatino'});//觸發(fā)器警報(bào)!style.get('font');//結(jié)果為"Palatino"style.font='icSans';//未觸發(fā)任何事件style.font;//結(jié)果為"icSans"style.get('font');//結(jié)果仍為"Palatino"(36|第2章分布式事件將來也許無需如此,前提是名為Object.observe的ECMAScript提案已經(jīng)獲得廣泛接納。aa.參見https://plus.google./111386188573471152118/posts/6peb6yffyWG。為此,Backbone的Model對象常常組織成Backbone集合的形式,其本質(zhì)是事件化數(shù)組。我們可以監(jiān)聽什么時(shí)候?qū)@些數(shù)組增減了Model對象。Backbone集合可以自動傳播其內(nèi)蘊(yùn)Model對象所發(fā)生的事件。舉個(gè)例子,假設(shè)有一個(gè)spriteCollection(精靈集合)集合對象包含了上百個(gè)Model對象,這些Model對象代表了要畫在canvas(畫布)元素上的一些東西。每當(dāng)任意一個(gè)精靈發(fā)生變化,都需要重新繪制畫布。我們不用逐個(gè)在那些精靈上附加redraw(重繪)函數(shù)作為change事件的處理器,相反,只要寫這樣一行代碼:spriteCollection.on('change',redraw);注意,集合事件的這種自動傳播只能下傳一層。Backbone沒有嵌套式集合這樣的概念。不過,我們可以自行用Backbone的trigger方法來實(shí)現(xiàn)嵌套式集合的多層傳播。有了多層傳播機(jī)制之后,任意的Backbone對象都可以觸發(fā)任意的事件。2.2.2事件循環(huán)與嵌套式變化從一個(gè)對象向另一個(gè)對象傳播事件的過程提出了一些需要關(guān)注的問題。如果每次有個(gè)對象上的事件引發(fā)了一系列事件并最終對這個(gè)對象本身觸發(fā)了相同的事件,則結(jié)果就是事件循環(huán)。如果這種事件循環(huán)還是同步的,那就造成了堆棧上溢,就像我們在2.1.3節(jié)中看到的一樣。然而在很多時(shí)候,變化事件的循環(huán)恰恰是我們想要的。最常見的情況(2.2事件化模型|37就是雙向綁定——兩個(gè)模型的取值會彼此關(guān)聯(lián)。假設(shè)我們想保證x始終等于2*y。varx=newBackbone.Model({value:0});vary=newBackbone.Model({value:0});x.on('change:value',function(x,xVal){y.set({value:xVal/2});});y.on('change:value',function(y,yVal){x.set({value:2*yVal});});你可能覺得當(dāng)x或y的取值變化時(shí),這段代碼會導(dǎo)致無限循環(huán)。但實(shí)際上它相當(dāng)安全,這要感謝Backbone中的兩道保險(xiǎn)。當(dāng)新值等于舊值時(shí),set方法不會導(dǎo)致觸發(fā)change事件。模型正處于自身的change事件期間時(shí),不會再觸發(fā)change事件。第二道保險(xiǎn)代表了一種自保哲學(xué)。假設(shè)模型的一個(gè)變化導(dǎo)致同一個(gè)模型又一次變化。由于第二次變化被“嵌套”在第一次變化內(nèi)部,所以這次變化的發(fā)生悄無聲息。外面的觀察者沒有機(jī)會回應(yīng)這種靜默的變化。很明顯,在Backbone中維持雙向數(shù)據(jù)綁定是一個(gè)挑戰(zhàn)。而另一個(gè)重要的MVC框架,即Ember.js

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論