這篇文章關(guān)于什么?
JAVAscript作為瀏覽器腳本語言,已經(jīng)逐漸變得無處不在,它讓你對(duì)事件驅(qū)動(dòng)模型有了基本理解,以及它與request-response模型的典型語言,如Ruby,Python和Java的區(qū)別,我將闡述一些關(guān)于JavaScript一致的核心概念,包括它的事件輪詢和消息隊(duì)列,希望能幫助你理解這門或許你不是完全理解的語言。
致讀者:
這篇文章寫給準(zhǔn)備使用Javascript進(jìn)行服務(wù)端/客戶端開發(fā)的web開發(fā)工作者(或者準(zhǔn)備從事該職業(yè)者),如果你對(duì)事件輪詢機(jī)制有比較好的了解,這篇文章或許你會(huì)覺得很熟悉。對(duì)于不是很了解事件輪詢機(jī)制的讀者,我希望能幫助你對(duì)這每天讀寫的代碼有一個(gè)基本的理解。
非阻塞I/O
在Javascript中,幾乎所有的I/O都是非阻塞的,其中包括http請(qǐng)求,數(shù)據(jù)操作以及磁盤讀寫;單線程詢問任務(wù)運(yùn)行時(shí)機(jī)以及執(zhí)行任務(wù),通過使用回調(diào)函數(shù),能讓Javascript線程在回調(diào)完成之前執(zhí)行其它任務(wù)。當(dāng)一個(gè)執(zhí)行完成的時(shí)候,會(huì)去執(zhí)行由回調(diào)函數(shù)提供的有序消息隊(duì)列中的下一個(gè)任務(wù)。
當(dāng)開發(fā)者熟悉了這種交互模式,用戶習(xí)慣了這種界面 — 當(dāng)事件發(fā)生,例如“mousedown”,“click”這種隨時(shí)可能被觸發(fā)的事件,它不同于同步機(jī)制,請(qǐng)求-響應(yīng)模型很少用在服務(wù)端應(yīng)用上。
讓我們比較兩塊代碼,它們分別發(fā)起HTTP請(qǐng)求于www.google.com然后在控制臺(tái)輸出響應(yīng)結(jié)果。首先,Ruby,使用Faraday:
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
執(zhí)行結(jié)果如下所示:
get方法已執(zhí)行,然而該線程直到有響應(yīng)才被回收
該來自Google的響應(yīng)返回的數(shù)據(jù)并沒有存在變量中
響應(yīng)結(jié)果輸出在控制臺(tái)中
直至最后,“任務(wù)完成”才出現(xiàn)在控制臺(tái)中
讓我們用Javascript的Node.js及它的Request庫來做同樣的事:
request('http://www.google.com', function(error, response, body) { console.log(body); }); console.log('Done!');
請(qǐng)求已被執(zhí)行,在請(qǐng)求得到響應(yīng)之前則已跳過一個(gè)匿名回調(diào)函數(shù)(該函數(shù)并未執(zhí)行)
“任務(wù)完成”馬上出現(xiàn)在控制臺(tái)中
一段時(shí)間之后,收到響應(yīng),此時(shí)回調(diào)函數(shù)才被執(zhí)行—在控制臺(tái)輸出響應(yīng)結(jié)果
事件輪詢
非耦合機(jī)制使得Javascript線程能在等待異步操作完成及其回調(diào)函數(shù)執(zhí)行之前執(zhí)行其它任務(wù)。那么,在內(nèi)存中在哪激活回調(diào)?回調(diào)按什么規(guī)則執(zhí)行?什么會(huì)讓回調(diào)執(zhí)行呢?
Javascript線程包括一個(gè)儲(chǔ)存了待執(zhí)行任務(wù)的消息列表的消息隊(duì)列,以及與它們相關(guān)聯(lián)的回調(diào)函數(shù),這些消息按照它們的響應(yīng)順序排列(例如鼠標(biāo)點(diǎn)擊,或者收到來自HTTP請(qǐng)求的響應(yīng))每條消息都有回調(diào)函數(shù),如果沒有提供回調(diào)函數(shù):例如當(dāng)用戶點(diǎn)擊一個(gè)按鈕但是沒有提供回調(diào)函數(shù),則沒有消息會(huì)被添加到消息隊(duì)列。
在每一次輪詢中,任務(wù)隊(duì)列會(huì)記錄下一條消息(每次記錄會(huì)返回一個(gè)“tick”),當(dāng)輪詢到這條消息時(shí),該消息所對(duì)應(yīng)的回調(diào)函數(shù)則被執(zhí)行。
在最初的架構(gòu)中,回調(diào)函數(shù)通過調(diào)用棧來實(shí)現(xiàn),由于Javascript是單線程的,消息隊(duì)列是阻塞的,對(duì)于后續(xù)任務(wù),必須等待之前的任務(wù)返回棧中所有回調(diào)函數(shù),才能將新任務(wù)的回調(diào)函數(shù)加入到棧中。在隨后的架構(gòu)中加入了函數(shù)(同步的)對(duì)棧的新的調(diào)用方法(此處例舉一個(gè)初始化為changeColor的函數(shù))。
function init() { var link = document.getElementById("foo"); link.addEventListener("click", function changeColor() { this.style.color = "burlywood"; }); } init();
在這個(gè)例子中,當(dāng)用戶點(diǎn)擊“foo”這個(gè)元素,“onclick”事件被觸發(fā),一個(gè)消息(以及回調(diào)函數(shù)changeColor)加入消息隊(duì)列。當(dāng)隊(duì)列按序執(zhí)行到該消息時(shí),它的回調(diào)函數(shù)changeColor被喚起。
當(dāng)回調(diào)函數(shù)changeColor返回(或者出錯(cuò)被丟掉),事件輪詢繼續(xù)執(zhí)行。只要與”foo”元素的onclick事件綁定的回調(diào)函數(shù)changeColor存在,隨后在該元素上的click事件都會(huì)使得更多的消息(及其回調(diào)函數(shù)changeColor)被加入隊(duì)列。
消息隊(duì)列的添加
如果你申明了一個(gè)異步函數(shù)(例如setTimeout),其回調(diào)函數(shù)最終會(huì)在一個(gè)不同的消息隊(duì)列中執(zhí)行,在未來的事件輪詢中的某個(gè)時(shí)刻。例如:
function f() { console.log("foo"); setTimeout(g, 0); console.log("baz"); h(); } function g() { console.log("bar"); } function h() { console.log("blix"); } f();
由于setTimeout非阻塞的本質(zhì),它的回調(diào)函數(shù)的未來的若干毫秒后執(zhí)行并且等待期間不占用該消息的進(jìn)程。在這個(gè)例子中,setTimeout跳過它的回調(diào)函數(shù)g和一段事件的延遲后被喚起。當(dāng)預(yù)先聲明的時(shí)間結(jié)束(在這個(gè)例子中幾乎是立即執(zhí)行)被分離出去的消息又被重新加回隊(duì)列,包括其回調(diào)函數(shù)g。這個(gè)回調(diào)函數(shù)被激活就好比:”foo”,”baz”,”blix”然后執(zhí)行下一個(gè)事件輪詢的tick:”bar”。如果在一個(gè)框架中同時(shí)聲明了兩個(gè)setTimeout,并且他們的第二個(gè)參數(shù)(執(zhí)行時(shí)間)想同,他們的回調(diào)將會(huì)按照其定義順序執(zhí)行。
Web Workers
利用Web Workers能讓你丟掉昂貴的多線程執(zhí)行方式,釋放主線程去做其他的事。Web Workers包括單獨(dú)的消息隊(duì)列,事件輪詢,以及實(shí)例化了一個(gè)獨(dú)立于最初的主線程的儲(chǔ)存空間。利用消息傳遞來建立消息與主線程之間的聯(lián)系,這種聯(lián)系非常像我們剛才的代碼示例。
首先,我們的worker:
// our worker, which does some CPU-intensive operation var reportResult = function(e) { pi = SomeLib.computePiToSpecifiedDecimals(e.data); postMessage(pi); }; onmessage = reportResult;
然后,這是在html中的代碼內(nèi)容:
// 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);
該示例中,主線程產(chǎn)生一個(gè)worker然后將一個(gè)logResult回調(diào)函數(shù)注冊(cè)到消息隊(duì)列中。在worker中,reportResult函數(shù)被注冊(cè)到它自己的消息事件中。當(dāng)worker線程從主線程接收消息時(shí),worker將消息及其相應(yīng)的回調(diào)函數(shù)加入隊(duì)列中。當(dāng)消息隊(duì)列按順序執(zhí)行到該消息時(shí),主線程將發(fā)回一條消息并將一條新的消息加入隊(duì)列(按照logResult的回調(diào)排序)由此開發(fā)者能讓CPU集中處理分線程,釋放主線程繼續(xù)處理消息任務(wù)及其綁定事件。
關(guān)于閉包
Javascript支持閉包,準(zhǔn)許注冊(cè)回調(diào),當(dāng)我們執(zhí)行回調(diào)時(shí),通過執(zhí)行回調(diào)創(chuàng)造的新的完全調(diào)用棧來維持我們創(chuàng)造的環(huán)境的入口。回調(diào)函數(shù)作為不同于我們創(chuàng)造的消息的一部分被調(diào)用。考慮如下示例:
function changeHeaderDeferred() { var header = document.getElementById("header"); setTimeout(function changeHeader() { header.style.color = "red"; return false; }, 100); return false; } changeHeaderDeferred();
在這個(gè)示例中,以頭變量方式聲明的changeHeaderDeferred函數(shù)被執(zhí)行。setTimeout函數(shù)被喚醒,導(dǎo)致消息(加在changeHeader回調(diào)中的)大約在100毫秒之后(時(shí)間偏差源于每臺(tái)計(jì)算機(jī)內(nèi)置原子鐘差異)添加到消息隊(duì)列,changeHeaderDeferred返回false,結(jié)束第一條消息的進(jìn)程,然而頭變量依然通過閉包的方式存在,沒有被垃圾回收機(jī)制回收。當(dāng)?shù)诙l消息執(zhí)行(changeHeader函數(shù))維持頭變量聲明的外部函數(shù)域的入口。一旦第二條消息(changeHeader函數(shù))執(zhí)行完畢,頭變量則被回收。
另外
Javascript的事件驅(qū)動(dòng)交互模型不同于大多數(shù)編程人員習(xí)慣的請(qǐng)求-響應(yīng)模型,但是你能看到,該技術(shù)也不是那么高不可攀。一個(gè)簡單的消息隊(duì)列及事件輪詢,Javascript使得開發(fā)者能夠圍繞收集異步回調(diào)的形式來建立他們的系統(tǒng),在等待外部事件發(fā)生的同時(shí)釋放主線程去做其它操作。它將越來越流行。
希望本文能幫助到您!
點(diǎn)贊+轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容(收藏不點(diǎn)贊,都是耍流氓-_-)
關(guān)注 {我},享受文章首發(fā)體驗(yàn)!
每周重點(diǎn)攻克一個(gè)前端技術(shù)難點(diǎn)。更多精彩前端內(nèi)容私信 我 回復(fù)“教程”
原文鏈接:http://eux.baidu.com/blog/fe/javascript-loop
作者:erin