1. 程式人生 > >Js事件迴圈機制(上)

Js事件迴圈機制(上)

最近琢磨了好久的Javascript的事件迴圈機制,看了很多國內的部落格總覺得寫的還是不夠深,很多都只說了Javascript的事件分為同步任務和非同步任務,遇到同步任務就放在執行棧中執行,而碰到非同步任務就放到任務佇列之中,等到執行棧執行完畢之後再去執行任務佇列之中的事件。自己對大概的基礎有所瞭解之後也沒接著深入去查資料,這就導致我在面試的時候被面試官一點一點深挖的時候就懵了(囧


函式呼叫棧與任務佇列

Javascript有一個main thread 主程序和call-stack(一個呼叫堆疊),在對一個呼叫堆疊中的task處理的時候,其他的都要等著。當在執行過程中遇到一些類似於setTimeout等非同步操作的時候,會交給瀏覽器的其他模組(以webkit為例,是webcore模組)進行處理,當到達setTimeout指定的延時執行的時間之後,task(回撥函式)會放入到任務佇列之中。一般不同的非同步任務的回撥函式會放入不同的任務佇列之中。等到呼叫棧中所有task執行完畢之後,接著去執行任務佇列之中的task(回撥函式)。


在上圖中,呼叫棧中遇到DOM操作、ajax請求以及setTimeout等WebAPIs的時候就會交給瀏覽器核心的其他模組進行處理,webkit核心在Javasctipt執行引擎之外,有一個重要的模組是webcore模組。對於圖中WebAPIs提到的三種API,webcore分別提供了DOM Binding、network、timer模組來處理底層實現。等到這些模組處理完這些操作的時候將回調函式放入任務佇列中,之後等棧中的task執行完之後再去執行任務佇列之中的回撥函式。

從setTimeout看事件迴圈機制

下面用Philip Roberts的演講中的一個栗子來說明事件迴圈機制究竟是怎麼執行setTimeout的。

首先main()函式的執行上下文入棧(對執行上下文還不瞭解的可以看我的上一篇部落格)。


程式碼接著執行,遇到console.log(‘Hi’),此時log(‘Hi’)入棧,console.log方法只是一個webkit核心支援的普通的方法,所以log(‘Hi’)方法立即被執行。此時輸出’Hi’。


當遇到setTimeout的時候,執行引擎將其新增到棧中。


呼叫棧發現setTimeout是之前提到的WebAPIs中的API,因此將其出棧之後將延時執行的函式交給瀏覽器的timer模組進行處理。


timer模組去處理延時執行的函式,此時執行引擎接著執行將log(‘SJS’)新增到棧中,此時輸出’SJS’。


當timer模組中延時方法規定的時間到了之後就將其放入到任務佇列之中,此時呼叫棧中的task已經全部執行完畢。


呼叫棧中的task執行完畢之後,執行引擎會接著看執行任務佇列中是否有需要執行的回撥函式。這裡的cb函式被執行引擎新增到呼叫棧中,接著執行裡面的程式碼,輸出’there’。等到執行結束之後再出棧。

小結

上面的這一個流程解釋了當瀏覽器遇到setTimeout之後究竟是怎麼執行的,相類似的還有前面圖中提到的另外的API以及另外一些非同步的操作。
總結上文說的,主要就是以下幾點:

  • 所有的程式碼都要通過函式呼叫棧中呼叫執行。
  • 當遇到前文中提到的APIs的時候,會交給瀏覽器核心的其他模組進行處理。
  • 任務佇列中存放的是回撥函式。
  • 等到呼叫棧中的task執行完之後再回去執行任務佇列之中的task。

測試

for (var i = 0; i < 5; i++) { 
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);

這段程式碼是我從網上前不久的一篇文章80%應聘者都不及格的 JS 面試題中找到的,現在我們就分析一下這段程式碼究竟是怎麼輸出最後文章中所說的最後的執行狀態:

40% 的人會描述為:5 -> 5,5,5,5,5,即第 1 個 5 直接輸出,1 秒之後,輸出 5 個 5;

  1. 首先i=0時,滿足條件,執行棧執行迴圈體裡面的程式碼,發現是setTimeout,將其出棧之後把延時執行的函式交給Timer模組進行處理。

  2. 當i=1,2,3,4時,均滿足條件,情況和i=0時相同,因此timer模組裡面有5個相同的延時執行的函式。

  3. 當i=5的時候,不滿足條件,因此for迴圈結束,console.log(new Date, i)入棧,此時的i已經變成了5。因此輸出5。

  4. 此時1s已經過去,timer模組將5個回撥函式按照註冊的順序返回給任務佇列。

  5. 執行引擎去執行任務佇列中的函式,5個function依次入棧執行之後再出棧,此時的i已經變成了5。因此幾乎同時輸出5個5。

  6. 因此等待的1s的時間其實只有輸出第一個5之後需要等待1s,這1s的時間是timer模組需要等到的規定的1s時間之後才將回撥函式交給任務佇列。等執行棧執行完畢之後再去執行任務對列中的5個回撥函式。這期間是不需要等待1s的。因此輸出的狀態就是:5 -> 5,5,5,5,5,即第1個 5 直接輸出,1s之後,輸出 5個5;

問題

看到這裡,對事件迴圈機制有了一個大概的瞭解了,可是細想,其中還有一些另外值得深入的問題。
下面通過一個栗子來說明:

(function test() { 
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()

在這段程式碼裡面,多了一個promise,那麼我們可以思考下面這個問題:

  1. promise的task會放在不同的任務佇列裡面,那麼setTimeout的任務佇列和promise的任務佇列的執行順序又是怎麼的呢?

  2. 到這裡大家看了我說了這麼多的task,那麼上文中一直提到的task究竟包括了什麼?具體是怎麼分的?

如果到這裡大家還是沒太懂的話,那麼接下來我會接著深入再細說不同task的事件迴圈機制。

當然,以上都是我自己鄙陋的見解,歡迎大家批評指正。