為什么JavaScript是單線程?
JavaScript語言的一大特點就是單線程毫胜,也就是說狂打,同一個時間只能做一件事擂煞。那么,為什么JavaScript不能有多個線程呢趴乡?這樣能提高效率啊对省。
JavaScript的單線程,與它的用途有關(guān)晾捏。作為瀏覽器腳本語言蒿涎,JavaScript的主要用途是與用戶互動,以及操作DOM惦辛。這決定了它只能是單線程劳秋,否則會帶來很復(fù)雜的同步問題。比如,假定JavaScript同時有兩個線程俗批,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點市怎,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)岁忘?
所以,為了避免復(fù)雜性区匠,從一誕生干像,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征驰弄,將來也不會改變麻汰。
為了利用多核CPU的計算能力,HTML5提出Web Worker標(biāo)準(zhǔn)戚篙,允許JavaScript腳本創(chuàng)建多個線程五鲫,但是子線程完全受主線程控制,且不得操作DOM岔擂。所以位喂,這個新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
任務(wù)隊列
單線程就意味著乱灵,所有任務(wù)需要排隊塑崖,前一個任務(wù)結(jié)束,才會執(zhí)行后一個任務(wù)痛倚。如果前一個任務(wù)耗時很長规婆,后一個任務(wù)就不得不一直等著。
所有任務(wù)可以分成兩種蝉稳,一種是同步任務(wù)(synchronous)抒蚜,另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是颠区,在主線程上排隊執(zhí)行的任務(wù)削锰,只有前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù)毕莱;異步任務(wù)指的是器贩,不進入主線程、而進入"任務(wù)隊列"(task queue)的任務(wù)朋截,只有"任務(wù)隊列"通知主線程蛹稍,某個異步任務(wù)可以執(zhí)行了,該任務(wù)才會進入主線程執(zhí)行部服。
具體來說唆姐,異步執(zhí)行的運行機制如下。(同步執(zhí)行也是如此廓八,因為它可以被視為沒有異步任務(wù)的異步執(zhí)行奉芦。)
(1)所有同步任務(wù)都在主線程上執(zhí)行赵抢,形成一個執(zhí)行棧(execution context stack)。
(2)主線程之外声功,還存在一個"任務(wù)隊列"(task queue)烦却。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件先巴。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢其爵,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件伸蚯。那些對應(yīng)的異步任務(wù)摩渺,于是結(jié)束等待狀態(tài),進入執(zhí)行棧剂邮,開始執(zhí)行摇幻。
(4)主線程不斷重復(fù)上面的第三步。
只要主線程空了挥萌,就會去讀取"任務(wù)隊列"囚企,這就是JavaScript的運行機制。這個過程會不斷重復(fù)瑞眼。
事件和回調(diào)函數(shù)
"任務(wù)隊列"是一個事件的隊列(也可以理解成消息的隊列)龙宏,IO設(shè)備完成一項任務(wù),就在"任務(wù)隊列"中添加一個事件伤疙,表示相關(guān)的異步任務(wù)可以進入"執(zhí)行棧"了银酗。主線程讀取"任務(wù)隊列",就是讀取里面有哪些事件徒像。
"任務(wù)隊列"中的事件黍特,除了IO設(shè)備的事件以外,還包括一些用戶產(chǎn)生的事件(比如鼠標(biāo)點擊锯蛀、頁面滾動等等)灭衷。只要指定過回調(diào)函數(shù),這些事件發(fā)生時就會進入"任務(wù)隊列"旁涤,等待主線程讀取翔曲。
所謂"回調(diào)函數(shù)"(callback),就是那些會被主線程掛起來的代碼劈愚。異步任務(wù)必須指定回調(diào)函數(shù)瞳遍,當(dāng)主線程開始執(zhí)行異步任務(wù),就是執(zhí)行對應(yīng)的回調(diào)函數(shù)菌羽。
"任務(wù)隊列"是一個先進先出的數(shù)據(jù)結(jié)構(gòu)掠械,排在前面的事件,優(yōu)先被主線程讀取。主線程的讀取過程基本上是自動的猾蒂,只要執(zhí)行棧一清空均唉,"任務(wù)隊列"上第一位的事件就自動進入主線程。但是肚菠,由于存在后文提到的"定時器"功能浸卦,主線程首先要檢查一下執(zhí)行時間,某些事件只有到了規(guī)定的時間案糙,才能返回主線程。
Event Loop
主線程從"任務(wù)隊列"中讀取事件靴庆,這個過程是循環(huán)不斷的时捌,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。
主線程運行的時候炉抒,產(chǎn)生堆(heap)和棧(stack)奢讨,棧中的代碼調(diào)用各種外部API,它們在"任務(wù)隊列"中加入各種事件(click焰薄,load拿诸,done)。只要棧中的代碼執(zhí)行完畢塞茅,主線程就會去讀取"任務(wù)隊列"亩码,依次執(zhí)行那些事件所對應(yīng)的回調(diào)函數(shù)。
執(zhí)行棧中的代碼(同步任務(wù))野瘦,總是在讀取"任務(wù)隊列"(異步任務(wù))之前執(zhí)行描沟。
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
上面代碼中的req.send方法是Ajax操作向服務(wù)器發(fā)送數(shù)據(jù),它是一個異步任務(wù)鞭光,意味著只有當(dāng)前腳本的所有代碼執(zhí)行完吏廉,系統(tǒng)才會去讀取"任務(wù)隊列"。所以惰许,它與下面的寫法等價
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
也就是說席覆,指定回調(diào)函數(shù)的部分(onload和onerror),在send()方法的前面或后面無關(guān)緊要汹买,因為它們屬于執(zhí)行棧的一部分佩伤,系統(tǒng)總是執(zhí)行完它們,才會去讀取"任務(wù)隊列"晦毙。
宏任務(wù)和微任務(wù)
頁面渲染事件畦戒,各種IO的完成事件等隨時被添加到任務(wù)隊列中,一直會保持先進先出的原則執(zhí)行结序,我們不能準(zhǔn)確地控制這些事件被添加到任務(wù)隊列中的位置障斋。但是這個時候突然有高優(yōu)先級的任務(wù)需要盡快執(zhí)行,那么一種類型的任務(wù)就不合適了,所以引入了微任務(wù)隊列垃环。
不同的異步任務(wù)被分為:宏任務(wù)和微任務(wù)
宏任務(wù):
- script(整體代碼)
- setTimeout()
- setInterval()
- postMessage
- I/O
- UI交互事件
微任務(wù):
- Promise.then
- Object.observe
- MutationObserver
- process.nextTick(Node.js 環(huán)境)
宏任務(wù)和微任務(wù)的執(zhí)行順序: