異步
簡(jiǎn)單地說(shuō),JavaScript 是單線(xiàn)程執行的語(yǔ)言,但在使用中有很多異步執行的情況。異步的本質(zhì)是用其他方式(相對同步)控制程序的執行順序,這與其他語(yǔ)言中的多線(xiàn)程模型不同,所以常常有人對非順序 JavaScript 代碼的運行結果感到困惑不解。
一段簡(jiǎn)單的小程序
任何使用過(guò) JavaScript 的程序員都能說(shuō)出下面這段代碼的輸出:
console.log("A");
setTimeout(() => {
console.log("B");
}, 100);
console.log("C");
先后順序是 A、C、B,因為第二個(gè)參數的作用是指定延遲的毫秒數,這段代碼只有一個(gè) setTimeout,所以不會(huì )讓人迷惑。
對類(lèi)似程序的解釋通常是由 setTimeout 設置一個(gè)定時(shí)器,在指定毫秒數后調用回調函數。然而,它的執行機制并不是這么簡(jiǎn)單。實(shí)際上,setTimeout 的作用是在指定的毫秒數之后,在得到機會(huì )時(shí),將 callback 放入 Event Loop Queue。
Event Loop
首先要拋出一些概念,通常所說(shuō)的 JavaScript Engine 是指負責執行一個(gè)一個(gè) chunk 的程序,它依賴(lài)宿主環(huán)境的調度,也需要通過(guò)宿主環(huán)境與操作系統產(chǎn)生關(guān)聯(lián)并得到支持。JavaScript Engine 是 JavaScript Runtime(Hosting Environment) 的一部分。
每個(gè) chunk 通常是以 function 為單位,一個(gè) chunk 執行完成后,才會(huì )執行下一個(gè) chunk。下一個(gè) chunk 是什么呢?取決于當前 Event Loop Queue 中的隊首。Event Loop Queue 中存放的都是消息,每個(gè)消息關(guān)聯(lián)著(zhù)一個(gè)函數,JavaScript Engine 就按照隊列中的消息順序執行它們,也就是執行 chunk。
所以上面的 setTimeout 實(shí)際執行起來(lái)更接近這樣:
chunk1執行:由 setTimeout 啟動(dòng)定時(shí)器(100毫秒)
chunk2執行:得到機會(huì ),將 callback 放入 Event Loop Queue
chunk3執行:此 callback 執行
不難發(fā)現,得到機會(huì )很重要!這也就可以解釋用 setTimeout 延遲 1000 不一定是準確的,而是會(huì )至少延遲一秒。因為如果還有其他的任務(wù)在前面,它要等待那些任務(wù)對應的消息都出隊,也就是程序都執行完成,它才能將 callback 放入隊列。也就是實(shí)際延遲會(huì )大于或等于一秒。
通常所說(shuō)的觸發(fā)了一個(gè)事件,就是指這個(gè) event listener 得到了執行。與 setTimeout 這個(gè)例子中的概念一樣,這也是一次 chunk 的執行。像這樣一個(gè)一個(gè)執行 chunk 的過(guò)程就叫 Event Loop。
還有一個(gè)經(jīng)常提到的概念叫「無(wú)阻塞」,JavaScript 中的無(wú)阻塞就是指這種 Event Loop 模型。除去 alert 或同步 Ajax 請求等歷史原因造成的問(wèn)題,程序總是不會(huì )出現阻塞;也就是說(shuō) JavaScript Engine 總是可以處理下一個(gè)任務(wù),如處理用戶(hù)對瀏覽器的操作。
一些簡(jiǎn)單的小例子
將 setTimeout 加入 try 語(yǔ)句之中,結果會(huì )如何?
try {
setTimeout(() => {
throw new Error("Error - from try statement");
}, 0);
} catch (e) {
console.error(e);
}
try catch 與 setTimeout 不在同一個(gè) chunk,所以……你懂的。
再看下一個(gè)。
下面的堆棧信息會(huì )輸出 C – B – A 嗎?
setTimeout(function A() {
setTimeout(function B() {
setTimeout(function C() {
throw new Error("Error - from function C");
}, 0);
}, 0);
}, 0);
它們并不對應同一條 Event Loop Queue 中的消息,分別有各自的調用棧,所以錯誤棧里面只有 C。
Job Queue
Job 是 ES6 中新增的概念,它與 Promise 的執行有關(guān),可以理解為等待執行的任務(wù);Job Queue 就是這種類(lèi)型的任務(wù)的隊列。JavaScript Runtime 對于 Job Queue 與 Event Loop Queue 的處理有所不同。
相同點(diǎn):
都用作先進(jìn)先出隊列
相異點(diǎn):
每個(gè) JavaScript Runtime 可以有多個(gè) Job Queue,但只有一個(gè) Event Loop Queue
當 JavaScript Engine 處理完當前 chunk 后,優(yōu)先執行所有的 Job Queue,然后再處理 Event Loop Queue
ES6 中,一個(gè) Promise 就是一個(gè) PromiseJob,一種 Job。
再來(lái)觀(guān)察一段小程序:
console.log("A");
setTimeout(() => {
console.log("A - setTimeout");
}, 0);
new Promise((resolve) => {
resolve();
})
.then(() => {
return console.log("A - Promise 1");
})
.then(() => {
return console.log("B - Promise 1");
});
new Promise((resolve) => {
resolve();
})
.then(() => {
return console.log("A - Promise 2");
})
.then(() => {
return console.log("B - Promise 2");
})
.t**粗體文字**hen(() => {
return console.log("C - Promise 2");
});
console.log("AA");
在原生支持 Promise 的環(huán)境,輸出是這樣:
A
AA
A – Promise 1
A – Promise 2
B – Promise 1
B – Promise 2
C – Promise 2
A – setTimeout
理解這個(gè)輸出:
A 與 AA 最先輸出,因為它們不是異步任務(wù),屬于第一個(gè) chunk。
Promise 1 與 Promise 2 先于 setTimeout 執行,因為 Job Queue 的執行優(yōu)先于 Event Loop Queue。
Promise 1 與 Promise 2 各自的輸出都是順序的,因為 Job Queue 是先進(jìn)先出隊列,同一 Job Queue 中的任務(wù)順序執行。
Promise 1 與 Promise 2 的后續任務(wù)是交錯的,因為 Promise 1 與 Promise 2 都是獨立的 PromiseJob(job 的其中一種),屬于不同的 Job Queue,它們之間的順序規范中沒(méi)有規定。
并發(fā)
文章開(kāi)頭,我說(shuō)「簡(jiǎn)單地說(shuō),JavaScript 是單線(xiàn)程執行的語(yǔ)言」,現在可以說(shuō)得稍微復雜一點(diǎn)了:JavaScript Engine 對 JavaScript 程序的執行是單線(xiàn)程的,但是 JavaScript Runtime(整個(gè)宿主環(huán)境)并不是單線(xiàn)程的;而且,幾乎所有的異步任務(wù)都是并發(fā)的,例如多個(gè) Job Queue、Ajax、Timer、I/O(Node)等等。
上面說(shuō)的是 JavaScript Runtime 層面,JavaScript 執行本身,也有一些特殊情況,例如:一個(gè) Web Worker 或者一個(gè)跨域的 iframe,也是獨立的線(xiàn)程,有各自的內存空間(棧、堆)以及 Event Loop Queue。要與這些不同的線(xiàn)程通信,只能通過(guò) postMessage。一次 postMessage 就是在另一個(gè)線(xiàn)程的 Event Loop Queue 中加入一條消息。
參考資料
Concurrency model and Event Loop
ECMAScript® 2015 Language Specification
You Don’t Know JS: Async & Performance
JavaScript異步編程:設計快速響應的網(wǎng)絡(luò )應用
本文由網(wǎng)上采集發(fā)布,不代表我們立場(chǎng),轉載聯(lián)系作者并注明出處:http://seensnowboarding.com/shbk/39387.html