眾所周知,JavaScript是單線程,同一時刻只會有一段代碼在運行。JavaScript又具有異步的特性,這二者是否沖突呢。JavaScript又是怎樣在單線程的情況下執行異步代碼的呢?
??但凡單線程且具備異步特性的語言,都是event-driven(事件驅動)的,而驅動這個事件(event-handler)的平臺即驅動平臺。瀏覽器就是一個驅動平臺,JavaScript雖然是單線程的,但是瀏覽器是多線程的,瀏覽器通過暴露給JavaScript一些API(WebAPI),來實現異步的功能。JavaScript里面常用到的setTimeout、Ajax、DOM Events就是WebAPI。
??瀏覽器有兩個核心部分,渲染引擎和JavaScript引擎。JavaScript主線程執行時候,產生對和棧。JavaScript異步的實現過程描述如下:
- 程序中的代碼一次進入棧,等待執行,執行完畢后出棧。
- 當執行到setTimeout等WebAPI時,將setTimeout方法壓棧,瀏覽器內核的其他線程開始執行WebAPI的callback,setTimeout出棧。
- JavaScript主線程繼續執行后續代碼(完成進棧出棧的過程),同時瀏覽器處理callback方法,當callback達到觸發條件時,方法被添加到任務隊列。
- 棧中代碼執行完畢后,主線程去查看任務隊列中,是否有callback等待執行,如果有,將callback方法壓入棧中,執行完畢后出棧。
以下代碼
alert(1);
setTimeout("alert(2)", 0);
for(var i = 3; i < 30; i++) {
alert(i);
}
運行,會發現,彈出1后并不會馬上彈出2,而是先彈出3-30,然后在彈出2,按照以上介紹的異步實現過程可以,程序會先執行alert(1),然后執行setTimeout方法,setTimeout方法是WebAPI,所以瀏覽器的其他線程會處理callback,callback方法0秒后就被觸發了,放入任務隊列,但是此時,JavaScript后續代碼還沒有執行完畢,JavaScript主線程會繼續執行后續代碼至全部執行完畢,此時棧為空,主線程才會將堆中的callback方法壓入棧中執行,所以setTimeout 0的callback不一定會0秒后執行。
以演講中的示例進一步說明(轉載地址
http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/#prettyPhoto)
以圖中代碼為例,執行引擎開始執行上述代碼時,相當于先講一個main()方法加入執行棧。繼續往下開始console.log('Hi')時,log('Hi')方法入棧,console.log方法是一個webkit內核支持的普通方法,而不是前面圖中WebAPIs涉及的方法,所以這里log('Hi')方法立即出棧被引擎執行。
console.log('Hi')語句執行完成后,log()方法出棧執行,輸出了Hi。引擎繼續往下,將setTimeout(callback,5000)添加到執行棧。setTimeout()方法屬于事件循環模型中WebAPIs中的方法,引擎在將setTimeout()方法出棧執行時,將延時執行的函數交給了相應模塊,即圖右方的timer模塊來處理。
執行引擎將setTimeout出棧執行時,將延時處理方法交由了webkit timer模塊處理,然后立即繼續往下處理后面代碼,于是將log('SJS')加入執行棧,接下來log('SJS')出棧執行,輸出SJS。而執行引擎在執行萬console.log('SJS')后,程序處理完畢,main()方法也出棧。