W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
單線程模型指的是,JavaScript只在一個(gè)線程上運(yùn)行。也就是說(shuō),JavaScript同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。
注意,JavaScript只在一個(gè)線程上運(yùn)行,不代表JavaScript引擎只有一個(gè)線程。事實(shí)上,JavaScript引擎有多個(gè)線程,單個(gè)腳本只能在一個(gè)線程上運(yùn)行,其他線程都是在后臺(tái)配合。
JavaScript之所以采用單線程,而不是多線程,跟歷史有關(guān)系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁(yè)腳本語(yǔ)言來(lái)說(shuō),這就太復(fù)雜了。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門(mén)語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線程的本質(zhì)。
單線程模型帶來(lái)了一些問(wèn)題,主要是新的任務(wù)被加在隊(duì)列的尾部,只有前面的所有任務(wù)運(yùn)行結(jié)束,才會(huì)輪到它執(zhí)行。如果有一個(gè)任務(wù)特別耗時(shí),后面的任務(wù)都會(huì)停在那里等待,造成瀏覽器失去響應(yīng),又稱“假死”。為了避免“假死”,當(dāng)某個(gè)操作在一定時(shí)間后仍無(wú)法結(jié)束,瀏覽器就會(huì)跳出提示框,詢問(wèn)用戶是否要強(qiáng)行停止腳本運(yùn)行。
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過(guò)來(lái),倒也算了,但是很多時(shí)候CPU是閑著的,因?yàn)镮O設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來(lái),再往下執(zhí)行。JavaScript語(yǔ)言的設(shè)計(jì)者意識(shí)到,這時(shí)CPU完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過(guò)頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是JavaScript內(nèi)部采用的Event Loop機(jī)制。
JavaScript運(yùn)行時(shí),除了一個(gè)運(yùn)行線程,引擎還提供一個(gè)消息隊(duì)列(message queue),里面是各種需要當(dāng)前程序處理的消息。新的消息進(jìn)入隊(duì)列的時(shí)候,會(huì)自動(dòng)排在隊(duì)列的尾端。
運(yùn)行線程只要發(fā)現(xiàn)消息隊(duì)列不為空,就會(huì)取出排在第一位的那個(gè)消息,執(zhí)行它對(duì)應(yīng)的回調(diào)函數(shù)。等到執(zhí)行完,再取出排在第二位的消息,不斷循環(huán),直到消息隊(duì)列變空為止。
每條消息與一個(gè)回調(diào)函數(shù)相聯(lián)系,也就是說(shuō),程序只要收到這條消息,就會(huì)執(zhí)行對(duì)應(yīng)的函數(shù)。另一方面,進(jìn)入消息隊(duì)列的消息,必須有對(duì)應(yīng)的回調(diào)函數(shù)。否則這個(gè)消息就會(huì)遺失,不會(huì)進(jìn)入消息隊(duì)列。舉例來(lái)說(shuō),鼠標(biāo)點(diǎn)擊就會(huì)產(chǎn)生一條消息,報(bào)告click
事件發(fā)生了。如果沒(méi)有回調(diào)函數(shù),這個(gè)消息就遺失了。如果有回調(diào)函數(shù),這個(gè)消息進(jìn)入消息隊(duì)列。等到程序收到這個(gè)消息,就會(huì)執(zhí)行click
事件的回調(diào)函數(shù)。
另一種情況是setTimeout
會(huì)在指定時(shí)間向消息隊(duì)列添加一條消息。如果消息隊(duì)列之中,此時(shí)沒(méi)有其他消息,這條消息會(huì)立即得到處理;否則,這條消息會(huì)不得不等到其他消息處理完,才會(huì)得到處理。因此,setTimeout
指定的執(zhí)行時(shí)間,只是一個(gè)最早可能發(fā)生的時(shí)間,并不能保證一定會(huì)在那個(gè)時(shí)間發(fā)生。
一旦當(dāng)前執(zhí)行??樟?,消息隊(duì)列就會(huì)取出排在第一位的那條消息,傳入程序。程序開(kāi)始執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),等到執(zhí)行完,再處理下一條消息。
所謂Event Loop機(jī)制,指的是一種內(nèi)部循環(huán),用來(lái)一輪又一輪地處理消息隊(duì)列之中的消息,即執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
Wikipedia的定義是:“Event Loop是一個(gè)程序結(jié)構(gòu),用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。可以就把Event Loop理解成動(dòng)態(tài)更新的消息隊(duì)列本身。
下面是一些常見(jiàn)的JavaScript任務(wù)。
所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。
同步任務(wù)指的是,在JavaScript執(zhí)行進(jìn)程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入JavaScript執(zhí)行進(jìn)程、而進(jìn)入“任務(wù)隊(duì)列”(task queue)的任務(wù),只有“任務(wù)隊(duì)列”通知主進(jìn)程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)(采用回調(diào)函數(shù)的形式)才會(huì)進(jìn)入JavaScript進(jìn)程執(zhí)行。
以Ajax操作為例,它可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理,由開(kāi)發(fā)者決定。如果是同步任務(wù),主線程就等著Ajax操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),該任務(wù)直接進(jìn)入“任務(wù)隊(duì)列”,JavaScript進(jìn)程跳過(guò)Ajax操作,直接往下執(zhí)行,等到Ajax操作有了結(jié)果,JavaScript進(jìn)程再執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
也就是說(shuō),雖然JavaScript只有一個(gè)進(jìn)程用來(lái)執(zhí)行,但是并行的還有其他進(jìn)程(比如,處理定時(shí)器的進(jìn)程、處理用戶輸入的進(jìn)程、處理網(wǎng)絡(luò)通信的進(jìn)程等等)。這些進(jìn)程通過(guò)向任務(wù)隊(duì)列添加任務(wù),實(shí)現(xiàn)與JavaScript進(jìn)程通信。
想要理解Event Loop,就要從程序的運(yùn)行模式講起。運(yùn)行以后的程序叫做“進(jìn)程”(process),一般情況下,一個(gè)進(jìn)程一次只能執(zhí)行一個(gè)任務(wù)。如果有很多任務(wù)需要執(zhí)行,不外乎三種解決方法。
排隊(duì)。因?yàn)橐粋€(gè)進(jìn)程一次只能執(zhí)行一個(gè)任務(wù),只好等前面的任務(wù)執(zhí)行完了,再執(zhí)行后面的任務(wù)。
新建進(jìn)程。使用fork命令,為每個(gè)任務(wù)新建一個(gè)進(jìn)程。
新建線程。因?yàn)檫M(jìn)程太耗費(fèi)資源,所以如今的程序往往允許一個(gè)進(jìn)程包含多個(gè)線程,由線程去完成任務(wù)。
如果某個(gè)任務(wù)很耗時(shí),比如涉及很多I/O(輸入/輸出)操作,那么線程的運(yùn)行大概是下面的樣子。
上圖的綠色部分是程序的運(yùn)行時(shí)間,紅色部分是等待時(shí)間??梢钥吹?,由于I/O操作很慢,所以這個(gè)線程的大部分運(yùn)行時(shí)間都在空等I/O操作的返回結(jié)果。這種運(yùn)行方式稱為”同步模式”(synchronous I/O)。
如果采用多線程,同時(shí)運(yùn)行多個(gè)任務(wù),那很可能就是下面這樣。
上圖表明,多線程不僅占用多倍的系統(tǒng)資源,也閑置多倍的資源,這顯然不合理。
上圖主線程的綠色部分,還是表示運(yùn)行時(shí)間,而橙色部分表示空閑時(shí)間。每當(dāng)遇到I/O的時(shí)候,主線程就讓Event Loop線程去通知相應(yīng)的I/O程序,然后接著往后運(yùn)行,所以不存在紅色的等待時(shí)間。等到I/O程序完成操作,Event Loop線程再把結(jié)果返回主線程。主線程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個(gè)任務(wù)。
可以看到,由于多出了橙色的空閑時(shí)間,所以主線程得以運(yùn)行更多的任務(wù),這就提高了效率。這種運(yùn)行方式稱為”異步模式“(asynchronous I/O)。
這正是JavaScript語(yǔ)言的運(yùn)行方式。單線程模型雖然對(duì)JavaScript構(gòu)成了很大的限制,但也因此使它具備了其他語(yǔ)言不具備的優(yōu)勢(shì)。如果部署得好,JavaScript程序是不會(huì)出現(xiàn)堵塞的,這就是為什么node.js平臺(tái)可以用很少的資源,應(yīng)付大流量訪問(wèn)的原因。
如果有大量的異步任務(wù)(實(shí)際情況就是這樣),它們會(huì)在“消息隊(duì)列”中產(chǎn)生大量的消息。這些消息排成隊(duì),等候進(jìn)入主線程。本質(zhì)上,“消息隊(duì)列”就是一個(gè)“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu)。比如,點(diǎn)擊鼠標(biāo)就產(chǎn)生一系列消息(各種事件),mousedown
事件排在mouseup
事件前面,mouseup
事件又排在click
事件的前面。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: