理解JS事件循環(huán)
伴隨著JavaScript這種web瀏覽器腳本語(yǔ)言的普及,對(duì)它的事件驅(qū)動(dòng)交互模型,以及它與Ruby、Python和Java中常見(jiàn)的請(qǐng)求-響應(yīng)模型的區(qū)別有一個(gè)基本了解,對(duì)您是有益的。在這篇文章中,我將解釋一些JavaScript并發(fā)模型的核心概念,包括其事件循環(huán)和消息隊(duì)列,希望能夠提升你對(duì)一種語(yǔ)言的理解,這種語(yǔ)言你可能已經(jīng)在使用但也許并不完全理解。
這篇文章是寫給誰(shuí)的?
這篇文章是針對(duì)在客戶端或服務(wù)器端使用或計(jì)劃使用JavaScript的web開(kāi)發(fā)人員的。如果你已經(jīng)精通事件循環(huán),那么這篇文章的大部分對(duì)你來(lái)說(shuō)會(huì)很熟悉。對(duì)于那些還不是很精通的人,我希望能給你提供一個(gè)基本的了解,這樣可以更好地幫助你閱讀和編寫日常代碼。
非阻塞I / O
在JavaScript中,幾乎所有的I/O都是非阻塞的。這包括HTTP請(qǐng)求,數(shù)據(jù)庫(kù)操作和磁盤讀寫,單線程執(zhí)行要求在運(yùn)行期執(zhí)行一個(gè)操作時(shí),提供一個(gè)回調(diào)函數(shù),然后繼續(xù)做其它的事情。當(dāng)操作已經(jīng)完成時(shí),消息和已提供的回調(diào)函數(shù)一起插入到隊(duì)列。在將來(lái)的某個(gè)時(shí)候,消息從隊(duì)列移除,回調(diào)函數(shù)觸發(fā)。
雖然這種交互模型可能對(duì)已經(jīng)習(xí)慣使用用戶界面的開(kāi)發(fā)人員很熟悉,比如“mousedown,”和“click”事件在某一時(shí)刻被觸發(fā)。這與通常在服務(wù)器端應(yīng)用程序進(jìn)行的同步式請(qǐng)求-響應(yīng)模型是不同的。
讓我們來(lái)比較一下兩小塊代碼,發(fā)出HTTP請(qǐng)求到www.google.com和輸出響應(yīng)到控制臺(tái)。首先看看Ruby,配合使用Faraday(一個(gè)Ruby 的HTTP 客戶端開(kāi)發(fā)庫(kù)):
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
執(zhí)行路徑很容易跟蹤:
1、執(zhí)行g(shù)et方法,執(zhí)行的線程等待,直到收到響應(yīng)
2、從谷歌收到響應(yīng)并返回給調(diào)用者,它存儲(chǔ)在一個(gè)變量中
3、變量的值(在本例中,就是我們的響應(yīng))輸出到控制臺(tái)
4、值“Done!“輸出到控制臺(tái)
讓我們使用Node.js和Request庫(kù)在JavaScript做同樣的事情:
request('http://www.google.com', function(error, response, body) { console.log(body); }); console.log('Done!');
表面上看略有不同,實(shí)際行為截然不同:
1、執(zhí)行請(qǐng)求函數(shù),傳遞一個(gè)匿名函數(shù)作為回調(diào),當(dāng)響應(yīng)在將來(lái)某個(gè)時(shí)候可用時(shí)執(zhí)行回調(diào)。
2、“Done!“立即輸出到控制臺(tái)
3、在將來(lái)的某個(gè)時(shí)候,響應(yīng)返回和回調(diào)執(zhí)行時(shí),輸出它的內(nèi)容到控制臺(tái)
事件循環(huán)
將調(diào)用者和響應(yīng)解耦,使得JavaScript在運(yùn)行期等待異步操作完成和回調(diào)觸發(fā)時(shí)可以做其他事情。但是這些回調(diào)在內(nèi)存中是如何組織的,按什么順序執(zhí)行?什么導(dǎo)致他們被調(diào)用?
JavaScript運(yùn)行時(shí)包含一個(gè)消息隊(duì)列,它存儲(chǔ)了需要處理的消息的列表和相關(guān)的回調(diào)函數(shù)。這些消息是以隊(duì)列的形式來(lái)響應(yīng)回調(diào)函數(shù)所涉及的外部事件(如鼠標(biāo)單擊或收到HTTP請(qǐng)求的響應(yīng))的。例如,如果用戶單擊一個(gè)按鈕,但沒(méi)有提供回調(diào)函數(shù),那么也沒(méi)有消息會(huì)被加入隊(duì)列。
在一次循環(huán),隊(duì)列提取下一條消息(每次提取稱為一次“tick”),當(dāng)事件發(fā)生,該消息的回調(diào)執(zhí)行。
回調(diào)函數(shù)的調(diào)用在調(diào)用棧作為初始化frame(片段),由于JavaScript是單線程的,未來(lái)的消息提取和處理因?yàn)榈却龡5乃姓{(diào)用返回而被停止。后續(xù)(同步)函數(shù)調(diào)用會(huì)添加新的調(diào)用frame到棧(例如,函數(shù)init調(diào)用函數(shù)changeColor)。
function init() { var link = document.getElementById("foo"); link.addEventListener("click", function changeColor() { this.style.color = "burlywood"; }); } init();
在這個(gè)例子中,當(dāng)用戶單擊“foo”元素時(shí),一條消息(及其回調(diào)函數(shù)changeColor)會(huì)被插入到隊(duì)列,并觸發(fā)“onclick“事件。當(dāng)消息離開(kāi)隊(duì)列時(shí),其回調(diào)函數(shù)changeColor被調(diào)用。當(dāng)changeColor返回(或者是拋出一個(gè)錯(cuò)誤),事件循環(huán)仍在繼續(xù)。只要函數(shù)changeColor存在,并指定為“foo”元素的onclick方法的回調(diào),那么在該元素上單擊會(huì)導(dǎo)致更多的消息(和相關(guān)的回調(diào)changeColor)插入隊(duì)列。
隊(duì)列附加消息
如果一個(gè)函數(shù)在代碼中按異步調(diào)用(比如setTimeout),提供的回調(diào)將最終作為一個(gè)不同的消息隊(duì)列的一部分被執(zhí)行,它將發(fā)生在事件循環(huán)的某個(gè)未來(lái)的動(dòng)作上。例如:
function f() { console.log("foo"); setTimeout(g, 0); console.log("baz"); h(); } function g() { console.log("bar"); } function h() { console.log("blix"); } f();
由于setTimeout的非阻塞特性,它的回調(diào)將在至少0毫秒后觸發(fā),而不是作為消息的一部分被處理。在這個(gè)示例中,setTimeout被調(diào)用, 傳入了一個(gè)回調(diào)函數(shù)g且延時(shí)0毫秒后執(zhí)行。當(dāng)我們指定時(shí)間到達(dá)(當(dāng)前情況是,幾乎立即執(zhí)行),一個(gè)單獨(dú)的消息將被加入隊(duì)列(g作為回調(diào)函數(shù))??刂婆_(tái)打印的結(jié)果會(huì)是像這樣:“foo”,“baz”,“blix”,然后是事件循環(huán)的下一個(gè)動(dòng)作:“bar”。如果在同一個(gè)調(diào)用片段中,兩個(gè)調(diào)用都設(shè)置為setTimeout -傳遞給第二個(gè)參數(shù)的值也相同-則它們的回調(diào)將按照調(diào)用順序插入隊(duì)列。
Web Workers
使用Web Workers允許您能夠?qū)⒁豁?xiàng)費(fèi)時(shí)的操作在一個(gè)單獨(dú)的線程中執(zhí)行,從而可以釋放主線程去做別的事情。worker(工作線程)包括一個(gè)獨(dú)立的消息隊(duì)列,事件循 環(huán),內(nèi)存空間獨(dú)立于實(shí)例化它的原始線程。worker和主線程之間的通信通過(guò)消息傳遞,看起來(lái)很像我們往常常見(jiàn)的傳統(tǒng)事件代碼示例。
首先,我們的worker:
// our worker, which does some CPU-intensive operation var reportResult = function(e) { pi = SomeLib.computePiToSpecifiedDecimals(e.data); postMessage(pi); }; onmessage = reportResult;
然后,主要的代碼塊在我們的HTML中以script-標(biāo)簽存在:
// our main code, in a <script>-tag in our HTML page var piWorker = new Worker("pi_calculator.js"); var logResult = function(e) { console.log("PI: " + e.data); }; piWorker.addEventListener("message", logResult, false); piWorker.postMessage(100000);
在這個(gè)例子中,主線程創(chuàng)建一個(gè)worker,同時(shí)注冊(cè)logResult回調(diào)函數(shù)到其“消息”事件。在worker里,reportResult函數(shù)注冊(cè)到自己的“消息”事件中。當(dāng)worker線程接收到主線程的消息,worker入隊(duì)一條消息同時(shí)帶上reportResult回調(diào)函數(shù)。消息出隊(duì)時(shí),一條新消息發(fā)送回主線程,新消息入隊(duì)主線程隊(duì)列(帶上logResult回調(diào)函數(shù))。這樣,開(kāi)發(fā)人員可以將cpu密集型操作委托給一個(gè)單獨(dú)的線程,使主線程解放出來(lái)繼續(xù)處理消息和事件。
關(guān)于閉包的
JavaScript對(duì)閉包的支持,允許你這樣注冊(cè)回調(diào)函數(shù),當(dāng)回調(diào)函數(shù)執(zhí)行時(shí),保持了對(duì)他們被創(chuàng)建的環(huán)境的訪問(wèn)(即使回調(diào)的執(zhí)行時(shí)創(chuàng)建了一個(gè)全新的調(diào)用棧)。理解我們的回調(diào)作為一個(gè)不同的消息的一部分被執(zhí)行,而不是創(chuàng)建它的那個(gè)會(huì)很有意思。看看下面的例子:
function changeHeaderDeferred() { var header = document.getElementById("header"); setTimeout(function changeHeader() { header.style.color = "red"; return false; }, 100); return false; } changeHeaderDeferred();
在這個(gè)例子中,changeHeaderDeferred函數(shù)被執(zhí)行時(shí)包含了變量header。函數(shù) setTimeout被調(diào)用,導(dǎo)致消息(帶上changeHeader回調(diào))被添加到消息隊(duì)列,在大約100毫秒后執(zhí)行。然后 changeHeaderDeferred函數(shù)返回false,結(jié)束第一個(gè)消息的處理,但header變量仍然可以通過(guò)閉包被引用,而不是被垃圾回收。當(dāng) 第二個(gè)消息被處理(changeHeader函數(shù)),它保持了對(duì)在外部函數(shù)作用域中聲明的header變量的訪問(wèn)。一旦第二個(gè)消息 (changeHeader函數(shù))執(zhí)行結(jié)束,header變量可以被垃圾回收。
提醒
JavaScript 事件驅(qū)動(dòng)的交互模型不同于許多程序員習(xí)慣的請(qǐng)求-響應(yīng)模型,但如你所見(jiàn),它并不復(fù)雜。使用簡(jiǎn)單的消息隊(duì)列和事件循環(huán),JavaScript使得開(kāi)發(fā)人員在構(gòu)建他們的系統(tǒng)時(shí)使用大量asynchronously-fired(異步-觸發(fā))回調(diào)函數(shù),讓運(yùn)行時(shí)環(huán)境能在等待外部事件觸發(fā)的同時(shí)處理并發(fā)操作。然而,這不過(guò)是并發(fā)的一種方法。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助。
- JavaScript運(yùn)行機(jī)制之事件循環(huán)(Event Loop)詳解
- javascript閉包傳參和事件的循環(huán)綁定示例探討
- 實(shí)例分析js事件循環(huán)機(jī)制
- 實(shí)例分析JS與Node.js中的事件循環(huán)
- 前端js中的事件循環(huán)eventloop機(jī)制詳解
- JS前端知識(shí)點(diǎn)總結(jié)之頁(yè)面加載事件,數(shù)組操作,DOM節(jié)點(diǎn)操作,循環(huán)和分支
- 詳解JS瀏覽器事件循環(huán)機(jī)制
- 關(guān)于js的事件循環(huán)機(jī)制剖析
相關(guān)文章
javascript中Date()函數(shù)在各瀏覽器中的顯示效果
本文給大家分享的是javascript中Date()函數(shù)在各瀏覽器中的顯示效果,由于各大瀏覽器的兼容性問(wèn)題,本文做了這個(gè)測(cè)試,希望有需要的小伙伴可以少走些彎路2015-06-06JavaScript實(shí)現(xiàn)倒計(jì)時(shí)功能2種方法實(shí)例
很多網(wǎng)站在做活動(dòng)時(shí)會(huì)出現(xiàn)一個(gè)截止時(shí)間倒計(jì)時(shí)的提示,下面這篇文章主要給大家介紹了JavaScript實(shí)現(xiàn)倒計(jì)時(shí)功能2種方法的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11js中eval()函數(shù)和trim()去掉字符串左右空格應(yīng)用
對(duì)于js中eval()函數(shù)的理解和寫一個(gè)函數(shù)trim()去掉字符串左右空格;對(duì)于js中eval()函數(shù)的理解是本人心得不一定正確,感興趣的朋友參考下,或許對(duì)你學(xué)習(xí)eval()函數(shù)有所幫助2013-02-02js 動(dòng)態(tài)生成html 觸發(fā)事件傳參字符轉(zhuǎn)義的實(shí)例
下面小編就為大家?guī)?lái)一篇js 動(dòng)態(tài)生成html 觸發(fā)事件傳參字符轉(zhuǎn)義的實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02利用原生js和jQuery實(shí)現(xiàn)單選框的勾選和取消操作的方法
下面小編就為大家?guī)?lái)一篇利用原生js和jQuery實(shí)現(xiàn)單選框的勾選和取消操作的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09詳解JavaScript數(shù)組過(guò)濾相同元素的5種方法
本篇文章主要介紹了詳解JavaScript數(shù)組過(guò)濾相同元素的5種方法,詳細(xì)的介紹了5種實(shí)用方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05JS實(shí)現(xiàn)數(shù)組去重,顯示重復(fù)元素及個(gè)數(shù)的方法示例
這篇文章主要介紹了JS實(shí)現(xiàn)數(shù)組去重,顯示重復(fù)元素及個(gè)數(shù)的方法,涉及javascript數(shù)組遍歷、統(tǒng)計(jì)、計(jì)算等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01