一、JS為何是單線程的?
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?這樣能提高效率啊。(在JAVA和c#中的異步均是通過多線程實現(xiàn)的,沒有循環(huán)隊列一說,直接在子線程中完成相關(guān)的操作)
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應該以哪個線程為準?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質(zhì)
二、JS是單線程的,那么他是如何是實現(xiàn)異步操作的?
JS的異步是通過回調(diào)函數(shù)實現(xiàn)的,即通過任務隊列,在主線程執(zhí)行完當前的任務棧(所有的同步操作),主線程空閑后輪詢?nèi)蝿贞犃?,并將任務隊列中的任務(回調(diào)函數(shù))取出來執(zhí)行。"回調(diào)函數(shù)"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調(diào)函數(shù),當主線程開始執(zhí)行異步任務,就是執(zhí)行對應的回調(diào)函數(shù)。
雖然JS是單線程的但是瀏覽器的內(nèi)核是多線程的,在瀏覽器的內(nèi)核中不同的異步操作由不同的瀏覽器內(nèi)核模塊調(diào)度執(zhí)行,異步操作會將相關(guān)回調(diào)添加到任務隊列中。而不同的異步操作添加到任務隊列的時機也不同,如 onclick, setTimeout, ajax 處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的 webcore 來執(zhí)行的,webcore 包含上圖中的3種 webAPI,分別是 DOM Binding、network、timer模塊。
// onclick 由瀏覽器內(nèi)核的 DOM Binding 模塊來處理,當事件觸發(fā)的時候,回調(diào)函數(shù)會立即添加到任務隊列中。
// setTimeout 會由瀏覽器內(nèi)核的 timer 模塊來進行延時處理,當時間到達的時候,才會將回調(diào)函數(shù)添加到任務隊列中。
// ajax 則會由瀏覽器內(nèi)核的 network 模塊來處理,在網(wǎng)絡(luò)請求完成返回之后,才將回調(diào)添加到任務隊列中。
JS中的異步運行機制如下:
(1)所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結(jié)果,就在"任務隊列"之中放置一個事件。
(3)一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結(jié)束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
(4)主線程不斷重復上面的第三步。
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復。(該過程又稱之為事件輪詢)
三、JS種事件隊列的優(yōu)先級
在JS中ES6 中新增的任務隊列(promise)是在事件循環(huán)之上的,事件循環(huán)每次 tick 后會查看 ES6 的任務隊列中是否有任務要執(zhí)行,也就是 ES6 的任務隊列比事件循環(huán)中的任務(事件)隊列優(yōu)先級更高。
如 Promise 就使用了 ES6 的任務隊列特性。也即在執(zhí)行完任務棧后首先執(zhí)行的是任務隊列中的promise任務。其他的上面常見的異步操作加入隊列的時間沒有相應的優(yōu)先級。
一個事件循環(huán)是可以有多個任務隊列的,每個任務都有一個任務源,相同任務源的任務只能在一個任務隊列中,一個任務隊列的執(zhí)行順序是先進先出,多個任務隊列有優(yōu)先級,但根據(jù)宿主環(huán)境的不同,優(yōu)先級也不同,不能保證固定。
es6標準中,任務又分為兩種類型,宏任務(macrotask)和微任務(microtask)。
宏任務:由宿主環(huán)境提供,比如setTimeout、setInterval、網(wǎng)絡(luò)請求、用戶I/O、script(整體代碼)、UI rendering、setImmediate(node)。
微任務:語言標準(ECMAScript)提供,如process.nextTick(node)、Promise.then()、Object.observe、MutationObserver。
console.log(1)
setTimeout(function () {
console.log(6)
}, 0)
new Promise(function (resolve, reject) {
console.log(2)
setTimeout(function () {
console.log(3)
}, 0)
resolve()
}).then(function (res) {
console.log(4)
})
console.log(5)
setTimeout(function () {
console.log(7)
}, 0)
new Promise(function (resolve, reject) {
console.log(8)
setTimeout(function () {
console.log(9)
new Promise(function (resolve, reject) {
console.log(11)
setTimeout(function () {
console.log(12)
}, 0)
resolve()
}).then(function (res) {
console.log(13)
})
}, 0)
resolve()
}).then(function (res) {
console.log(10)
})
//1 2 5 8 4 10 6 3 7 9 11 13 12