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