摘要:例如處理請(qǐng)求的線程處理事件的線程定時(shí)器線程讀寫(xiě)文件的線程例如在中等等。事件循環(huán)事件循環(huán)是指主線程重復(fù)從消息隊(duì)列中取消息執(zhí)行的過(guò)程。事件觸發(fā)時(shí),表示異步任務(wù)完成,會(huì)將事件監(jiān)聽(tīng)器函數(shù)封裝成一條消息放到消息隊(duì)列中,等待主線程執(zhí)行。
一. 單線程
我們常說(shuō)“JavaScript是單線程的”。
所謂單線程,是指在JS引擎中負(fù)責(zé)解釋和執(zhí)行JavaScript代碼的線程只有一個(gè)。不妨叫它主線程。
但是實(shí)際上還存在其他的線程。例如:處理AJAX請(qǐng)求的線程、處理DOM事件的線程、定時(shí)器線程、讀寫(xiě)文件的線程(例如在Node.js中)等等。這些線程可能存在于JS引擎之內(nèi),也可能存在于JS引擎之外,在此我們不做區(qū)分。不妨叫它們工作線程。
二. 同步和異步假設(shè)存在一個(gè)函數(shù)A:
A(args...);
同步:如果在函數(shù)A返回的時(shí)候,調(diào)用者就能夠得到預(yù)期結(jié)果(即拿到了預(yù)期的返回值或者看到了預(yù)期的效果),那么這個(gè)函數(shù)就是同步的。
例如:
Math.sqrt(2); console.log("Hi");
第一個(gè)函數(shù)返回時(shí),就拿到了預(yù)期的返回值:2的平方根。
第二個(gè)函數(shù)返回時(shí),就看到了預(yù)期的效果:在控制臺(tái)打印了一個(gè)字符串。
所以這兩個(gè)函數(shù)都是同步的。
異步:如果在函數(shù)A返回的時(shí)候,調(diào)用者還不能夠得到預(yù)期結(jié)果,而是需要在將來(lái)通過(guò)一定的手段得到,那么這個(gè)函數(shù)就是異步的。
例如:
fs.readFile("foo.txt", "utf8", function(err, data) { console.log(data); });
在上面的代碼中,我們希望通過(guò)fs.readFile函數(shù)讀取文件foo.txt中的內(nèi)容,并打印出來(lái)。
但是在fs.readFile函數(shù)返回時(shí),我們期望的結(jié)果并不會(huì)發(fā)生,而是要等到文件全部讀取完成之后。如果文件很大的話可能要很長(zhǎng)時(shí)間。
下面以AJAX請(qǐng)求為例,來(lái)看一下同步和異步的區(qū)別:
異步AJAX:
主線程:“你好,AJAX線程。請(qǐng)你幫我發(fā)個(gè)HTTP請(qǐng)求吧,我把請(qǐng)求地址和參數(shù)都給你了?!?/p>
AJAX線程:“好的,主線程。我馬上去發(fā),但可能要花點(diǎn)兒時(shí)間呢,你可以先去忙別的?!?/p>
主線程::“謝謝,你拿到響應(yīng)后告訴我一聲啊。”
(接著,主線程做其他事情去了。一頓飯的時(shí)間后,它收到了響應(yīng)到達(dá)的通知。)
同步AJAX:
主線程:“你好,AJAX線程。請(qǐng)你幫我發(fā)個(gè)HTTP請(qǐng)求吧,我把請(qǐng)求地址和參數(shù)都給你了?!?/p>
AJAX線程:“......”
主線程::“喂,AJAX線程,你怎么不說(shuō)話?”
AJAX線程:“......”
主線程::“喂!喂喂喂!”
AJAX線程:“......”
(一炷香的時(shí)間后)
主線程::“喂!求你說(shuō)句話吧!”
AJAX線程:“主線程,不好意思,我在工作的時(shí)候不能說(shuō)話。你的請(qǐng)求已經(jīng)發(fā)完了,拿到響應(yīng)數(shù)據(jù)了,給你。”
正是由于JavaScript是單線程的,而異步容易實(shí)現(xiàn)非阻塞,所以在JavaScript中對(duì)于耗時(shí)的操作或者時(shí)間不確定的操作,使用異步就成了必然的選擇。異步是這篇文章關(guān)注的重點(diǎn)。
三. 異步過(guò)程的構(gòu)成要素從上文可以看出,異步函數(shù)實(shí)際上很快就調(diào)用完成了。但是后面還有工作線程執(zhí)行異步任務(wù)、通知主線程、主線程調(diào)用回調(diào)函數(shù)等很多步驟。我們把整個(gè)過(guò)程叫做異步過(guò)程。異步函數(shù)的調(diào)用在整個(gè)異步過(guò)程中,只是一小部分。
總結(jié)一下,一個(gè)異步過(guò)程通常是這樣的:
主線程發(fā)起一個(gè)異步請(qǐng)求,相應(yīng)的工作線程接收請(qǐng)求并告知主線程已收到(異步函數(shù)返回);主線程可以繼續(xù)執(zhí)行后面的代碼,同時(shí)工作線程執(zhí)行異步任務(wù);工作線程完成工作后,通知主線程;主線程收到通知后,執(zhí)行一定的動(dòng)作(調(diào)用回調(diào)函數(shù))。
異步函數(shù)通常具有以下的形式:
A(args..., callbackFn)
它可以叫做異步過(guò)程的發(fā)起函數(shù),或者叫做異步任務(wù)注冊(cè)函數(shù)。args是這個(gè)函數(shù)需要的參數(shù)。callbackFn也是這個(gè)函數(shù)的參數(shù),但是它比較特殊所以多帶帶列出來(lái)。
所以,從主線程的角度看,一個(gè)異步過(guò)程包括下面兩個(gè)要素:
發(fā)起函數(shù)(或叫注冊(cè)函數(shù))A
回調(diào)函數(shù)callbackFn
它們都是在主線程上調(diào)用的,其中注冊(cè)函數(shù)用來(lái)發(fā)起異步過(guò)程,回調(diào)函數(shù)用來(lái)處理結(jié)果。
舉個(gè)具體的例子:
setTimeout(fn, 1000);
其中的setTimeout就是異步過(guò)程的發(fā)起函數(shù),fn是回調(diào)函數(shù)。
注意:前面說(shuō)的形式A(args..., callbackFn)只是一種抽象的表示,并不代表回調(diào)函數(shù)一定要作為發(fā)起函數(shù)的參數(shù),例如:
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = xxx; // 添加回調(diào)函數(shù) xhr.open("GET", url); xhr.send(); // 發(fā)起函數(shù)
發(fā)起函數(shù)和回調(diào)函數(shù)就是分離的。
四. 消息隊(duì)列和事件循環(huán)上文講到,異步過(guò)程中,工作線程在異步操作完成后需要通知主線程。那么這個(gè)通知機(jī)制是怎樣實(shí)現(xiàn)的呢?答案是利用消息隊(duì)列和事件循環(huán)。
用一句話概括:
工作線程將消息放到消息隊(duì)列,主線程通過(guò)事件循環(huán)過(guò)程去取消息。
消息隊(duì)列:消息隊(duì)列是一個(gè)先進(jìn)先出的隊(duì)列,它里面存放著各種消息。
事件循環(huán):事件循環(huán)是指主線程重復(fù)從消息隊(duì)列中取消息、執(zhí)行的過(guò)程。
實(shí)際上,主線程只會(huì)做一件事情,就是從消息隊(duì)列里面取消息、執(zhí)行消息,再取消息、再執(zhí)行。當(dāng)消息隊(duì)列為空時(shí),就會(huì)等待直到消息隊(duì)列變成非空。而且主線程只有在將當(dāng)前的消息執(zhí)行完成后,才會(huì)去取下一個(gè)消息。這種機(jī)制就叫做事件循環(huán)機(jī)制,取一個(gè)消息并執(zhí)行的過(guò)程叫做一次循環(huán)。
事件循環(huán)用代碼表示大概是這樣的:
while(true) { var message = queue.get(); execute(message); }
那么,消息隊(duì)列中放的消息具體是什么東西?消息的具體結(jié)構(gòu)當(dāng)然跟具體的實(shí)現(xiàn)有關(guān),但是為了簡(jiǎn)單起見(jiàn),我們可以認(rèn)為:
消息就是注冊(cè)異步任務(wù)時(shí)添加的回調(diào)函數(shù)。
再次以異步AJAX為例,假設(shè)存在如下的代碼:
$.ajax("http://segmentfault.com", function(resp) { console.log("我是響應(yīng):", resp); }); // 其他代碼 ... ... ...
主線程在發(fā)起AJAX請(qǐng)求后,會(huì)繼續(xù)執(zhí)行其他代碼。AJAX線程負(fù)責(zé)請(qǐng)求segmentfault.com,拿到響應(yīng)后,它會(huì)把響應(yīng)封裝成一個(gè)JavaScript對(duì)象,然后構(gòu)造一條消息:
// 消息隊(duì)列中的消息就長(zhǎng)這個(gè)樣子 var message = function () { callbackFn(response); }
其中的callbackFn就是前面代碼中得到成功響應(yīng)時(shí)的回調(diào)函數(shù)。
主線程在執(zhí)行完當(dāng)前循環(huán)中的所有代碼后,就會(huì)到消息隊(duì)列取出這條消息(也就是message函數(shù)),并執(zhí)行它。到此為止,就完成了工作線程對(duì)主線程的通知,回調(diào)函數(shù)也就得到了執(zhí)行。如果一開(kāi)始主線程就沒(méi)有提供回調(diào)函數(shù),AJAX線程在收到HTTP響應(yīng)后,也就沒(méi)必要通知主線程,從而也沒(méi)必要往消息隊(duì)列放消息。
用圖表示這個(gè)過(guò)程就是:
從上文中我們也可以得到這樣一個(gè)明顯的結(jié)論,就是:
五. 異步與事件異步過(guò)程的回調(diào)函數(shù),一定不在當(dāng)前這一輪事件循環(huán)中執(zhí)行。
上文中說(shuō)的“事件循環(huán)”,為什么里面有個(gè)事件呢?那是因?yàn)椋?/p>
消息隊(duì)列中的每條消息實(shí)際上都對(duì)應(yīng)著一個(gè)事件。
上文中一直沒(méi)有提到一類(lèi)很重要的異步過(guò)程:DOM事件。
舉例來(lái)說(shuō):
var button = document.getElement("#btn"); button.addEventListener("click", function(e) { console.log(); });
從事件的角度來(lái)看,上述代碼表示:在按鈕上添加了一個(gè)鼠標(biāo)單擊事件的事件監(jiān)聽(tīng)器;當(dāng)用戶點(diǎn)擊按鈕時(shí),鼠標(biāo)單擊事件觸發(fā),事件監(jiān)聽(tīng)器函數(shù)被調(diào)用。
從異步過(guò)程的角度看,addEventListener函數(shù)就是異步過(guò)程的發(fā)起函數(shù),事件監(jiān)聽(tīng)器函數(shù)就是異步過(guò)程的回調(diào)函數(shù)。事件觸發(fā)時(shí),表示異步任務(wù)完成,會(huì)將事件監(jiān)聽(tīng)器函數(shù)封裝成一條消息放到消息隊(duì)列中,等待主線程執(zhí)行。
事件的概念實(shí)際上并不是必須的,事件機(jī)制實(shí)際上就是異步過(guò)程的通知機(jī)制。我覺(jué)得它的存在是為了編程接口對(duì)開(kāi)發(fā)者更友好。
另一方面,所有的異步過(guò)程也都可以用事件來(lái)描述。例如:setTimeout可以看成對(duì)應(yīng)一個(gè)時(shí)間到了!的事件。前文的setTimeout(fn, 1000);可以看成:
timer.addEventListener("timeout", 1000, fn);六. 生產(chǎn)者與消費(fèi)者
從生產(chǎn)者與消費(fèi)者的角度看,異步過(guò)程是這樣的:
七. 總結(jié)一下工作線程是生產(chǎn)者,主線程是消費(fèi)者(只有一個(gè)消費(fèi)者)。工作線程執(zhí)行異步任務(wù),執(zhí)行完成后把對(duì)應(yīng)的回調(diào)函數(shù)封裝成一條消息放到消息隊(duì)列中;主線程不斷地從消息隊(duì)列中取消息并執(zhí)行,當(dāng)消息隊(duì)列空時(shí)主線程阻塞,直到消息隊(duì)列再次非空。
最后再用一個(gè)生活中的例子總結(jié)一下同步和異步:在公路上,汽車(chē)一輛接一輛,有條不紊的運(yùn)行。這時(shí),有一輛車(chē)壞掉了。假如它停在原地進(jìn)行修理,那么后面的車(chē)就會(huì)被堵住沒(méi)法行駛,交通就亂套了。幸好旁邊有應(yīng)急車(chē)道,可以把故障車(chē)輛推到應(yīng)急車(chē)道修理,而正常的車(chē)流不會(huì)受到任何影響。等車(chē)修好了,再?gòu)膽?yīng)急車(chē)道回到正常車(chē)道即可。唯一的影響就是,應(yīng)急車(chē)道用多了,原來(lái)的車(chē)輛之間的順序會(huì)有點(diǎn)亂。
這就是同步和異步的區(qū)別。同步可以保證順序一致,但是容易導(dǎo)致阻塞;異步可以解決阻塞問(wèn)題,但是會(huì)改變順序性。改變順序性其實(shí)也沒(méi)有什么大不了的,只不過(guò)讓程序變得稍微難理解了一些 :)
PS:ECMAScript 262規(guī)范中,并沒(méi)有對(duì)異步、事件隊(duì)列等概念及其實(shí)現(xiàn)的描述。這些都是具體的JavaScript運(yùn)行時(shí)環(huán)境使用的機(jī)制。本文重點(diǎn)是描述異步過(guò)程的原理,為了便于理解做了很多簡(jiǎn)化。所以文中的某些術(shù)語(yǔ)的使用可能是不準(zhǔn)確的,具體細(xì)節(jié)也未必是正確的,例如消息隊(duì)列中消息的結(jié)構(gòu)。請(qǐng)讀者注意。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/78500.html
摘要:現(xiàn)實(shí)中是這樣的執(zhí)行結(jié)果為結(jié)果告訴我們,是單線程沒(méi)錯(cuò),不過(guò)不是逐行同步執(zhí)行。搜索了很多官方個(gè)人博客得到了一堆詞引擎主線程事件表事件隊(duì)列宏任務(wù)微任務(wù),徹底懵逼。。。以此規(guī)則不停的執(zhí)行下去就是我們所聽(tīng)到的事件循環(huán)。 都知道javascript是單線程,那么問(wèn)題來(lái)了,既然是單線程順序執(zhí)行,那怎么做到異步的呢? 我們理解的單線程應(yīng)該是這樣的,排著一個(gè)個(gè)來(lái),是同步執(zhí)行。 showImg(https...
摘要:心塞塞根據(jù)規(guī)范,事件循環(huán)是通過(guò)任務(wù)隊(duì)列的機(jī)制來(lái)進(jìn)行協(xié)調(diào)的。等便是任務(wù)源,而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)回調(diào)函數(shù)。然后當(dāng)前本輪的結(jié)束,主線程可以繼續(xù)取下一個(gè)執(zhí)行。 依然是:經(jīng)濟(jì)基礎(chǔ)決定上層建筑。 說(shuō)明 首先,旨在搞清常用的同步異步執(zhí)行機(jī)制 其次,暫時(shí)不討論node.js的Event Loop執(zhí)行機(jī)制,以下關(guān)于瀏覽器的Event Loop執(zhí)行機(jī)制 最后,借鑒了很多前輩的研究文...
摘要:想必面試題刷的多的同學(xué)對(duì)下面這道題目不陌生,能夠立即回答出輸出個(gè),可是你真的懂為什么嗎為什么是輸出為什么是輸出個(gè)這兩個(gè)問(wèn)題在我腦邊縈繞。同步任務(wù)都好理解,一個(gè)執(zhí)行完執(zhí)行下一個(gè)。本文只是我對(duì)這道面試題的一點(diǎn)思考,有誤的地方望批評(píng)指正。 想必面試題刷的多的同學(xué)對(duì)下面這道題目不陌生,能夠立即回答出輸出10個(gè)10,可是你真的懂為什么嗎?為什么是輸出10?為什么是輸出10個(gè)10?這兩個(gè)問(wèn)題在我腦...
摘要:事件完成,回調(diào)函數(shù)進(jìn)入。主線程從讀取回調(diào)函數(shù)并執(zhí)行。終于執(zhí)行完了,終于從進(jìn)入了主線程執(zhí)行。遇到,立即執(zhí)行。宏任務(wù)微任務(wù)第三輪事件循環(huán)宏任務(wù)執(zhí)行結(jié)束,執(zhí)行兩個(gè)微任務(wù)和。事件循環(huán)事件循環(huán)是實(shí)現(xiàn)異步的一種方法,也是的執(zhí)行機(jī)制。 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果讀完本文還不懂,可以揍我。不論你是javascript新手還是老鳥(niǎo),不論是面試求職,還是日常開(kāi)發(fā)工作...
摘要:關(guān)于這部分有嚴(yán)格的文字定義,但本文的目的是用最小的學(xué)習(xí)成本徹底弄懂執(zhí)行機(jī)制,所以同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行場(chǎng)所,同步的進(jìn)入主線程,異步的進(jìn)入并注冊(cè)函數(shù)。宏任務(wù)微任務(wù)第三輪事件循環(huán)宏任務(wù)執(zhí)行結(jié)束,執(zhí)行兩個(gè)微任務(wù)和。 不論你是javascript新手還是老鳥(niǎo),不論是面試求職,還是日常開(kāi)發(fā)工作,我們經(jīng)常會(huì)遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內(nèi)容和順序。 因?yàn)閖avascr...
閱讀 1142·2021-11-15 18:11
閱讀 3238·2021-09-22 15:33
閱讀 3541·2021-09-01 11:42
閱讀 2726·2021-08-24 10:03
閱讀 3684·2021-07-29 13:50
閱讀 2984·2019-08-30 14:08
閱讀 1336·2019-08-28 17:56
閱讀 2336·2019-08-26 13:57