平時的工做中,也許你會常常用到setTimeout這個方法,但是你真的瞭解setTimeout嗎?本文想經過總結setTimeout的用法,順便來探索javascript裏面的事件執行機制。javascript
一、html
setTimeout(code,millisec)
setTimeout函數接受兩個參數,第一個參數code是將要推遲執行的函數名或者一段代碼,第二個參數millisec是推遲執行的毫秒數。java
例如:web
setTimeout(‘console.log(2)’,100); setTimeout(function(){console.log(2)},100);
若是直接在setTimeout中直接執行代碼, 須要以字符串的形式去寫,引擎內部會將字符串轉爲可執行的代碼segmentfault
二、再來一些簡單些的代碼瀏覽器
console.log(1); setTimeout('console.log(2)',1000); console.log(3);
是的,如你所願,依次輸出的是 // 1 3 2異步
三、代碼升級版函數
console.log(1); setTimeout(function(){ console.log(2); },300); setTimeout(function(){ console.log(3) },400); for (var i = 0;i<10000;i++) { console.log(4); } setTimeout(function(){ console.log(5); },100);
這個時候的輸入順序是怎樣的呢?這裏先埋個伏筆,由於咱們是以setTimeout來聊Event Loopoop
什麼是Event Loop呢?post
由於javascript是單線程的,所謂的單線程是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個,能夠叫它爲主線程。
除了主線程,還存在其餘的線程。例如:處理AJAX請求的線程、處理DOM事件的線程、定時器線程、讀寫文件的線程(例如在Node.js中)等等。咱們以setTimeout爲例,當在代碼中調用setTimeout()方法時,註冊的延時方法會交由瀏覽器內核其餘模塊(以webkit爲例,是webcore模塊)處理,當延時方法到達觸發條件,即到達設置的延時時間時,這一延時方法被添加至任務隊列裏。這一過程由瀏覽器內核其餘模塊處理,與執行引擎主線程獨立,執行引擎在主線程方法執行完畢,到達空閒狀態時,會從任務隊列中順序獲取任務來執行,這一過程是一個不斷循環的過程,稱爲事件循環模型。
Javascript執行引擎的主線程運行的時候,產生堆(heap)和棧(stack)。程序中代碼依次進入棧中等待執行,當調用setTimeout()方法時,即圖中右側WebAPIs方法時,瀏覽器內核相應模塊開始延時方法的處理,當延時方法到達觸發條件時,方法被添加到用於回調的任務隊列,只有執行引擎棧中的代碼執行完畢,主線程纔會去讀取任務隊列,依次執行那些知足觸發條件的回調函數。
在上圖中的callback queue中指的是 "任務隊列",也能夠理解爲消息的隊列,「消息「咱們能夠簡單理解爲是:註冊異步任務時添加的回調函數。
例如:
setTimeout(function(){ console.log(‘hello’); },100);
其中裏面的function(){console.log('hello')}就是一個消息,任務隊列裏面保存的就是這些回調函數
咱們以一段代碼的運行來進行理解,代碼以下:
console.log('start'); //Timer1 setTimeout(function(){ console.log('hello'); },200); //Timer2 setTimeout(function(){ console.log('world'); },100); console.log('end');
代碼運行的gif圖以下:
咱們分步驟來進行這個過程解答
一、 js執行引擎開始執行上述代碼時,會先講一個main()方法加入執行棧。首先第一個console.log(‘start’)入棧,console.log方法是一個webkit內核支持的普通方法,而不是前面圖中WebAPIs涉及的方法,因此這裏log('start')方法當即出棧被引擎執行。
二、引擎繼續往下,將setTimeout(callback,200)添加到執行棧。setTimeout()方法屬於事件循環模型中WebAPIs中的方法,引擎在將setTimeout()方法出棧執行時,將延時執行的函數交給了相應模塊,即圖右方的timer模塊來處理。
三、而後主線程繼續向下執行,緊接着將第二個定時器也交給Timer模塊,而後執行到第二個console.log(),控制檯打印'end',
四、執行完畢後清空執行棧。可是並無結束,在主線程執行的同時,Timer模塊會檢查其中的異步代碼,一旦知足觸發條件,就會將它添加到任務隊列中。Timer2延遲100ms,因此會早於Timer1被添加到隊列排頭。而主線程此時處於空閒狀態,因此會檢查任務隊列是否有待執行的任務。此時會將Timer2回調中的console.log()執行,控制檯打印'world',而後執行棧空閒後繼續檢查任務隊列,將Timer1的代碼壓入執行棧中執行,控制檯打印'hello',清空執行棧,此時任務隊列爲空,執行結束,程序處理完畢,main()方法也出棧。
五、在這裏再次強調一下,不是setTimeout加入了事件隊列,而是setTimeout裏面的回調函數加入了事件隊列
回到咱們文章之初的那倒題:
console.log(1); //Time1 setTimeout(function(){ console.log(2); },300); //Time2 setTimeout(function(){ console.log(3) },400); for (var i = 0;i<10000;i++) { console.log(4); }
//Time3 setTimeout(function(){ console.log(5); },100);
若是理解了上面的內容,那麼這道題理解起來就比較容易了。
首先是打印出 1,而後是 10000個4,那麼Time一、Time二、Time3是順序是如何的呢?
在這個代碼中,for循環比較耗時,在Time1和Timer加入到執行隊列中後,主線程依然還在執行for循環中的代碼,處於阻塞狀態。隊列中的Time1和Time2並不會得以執行。當for循環結束,這時纔將Time3交由Timer模塊去管理,清空執行棧。雖然在這裏Time3的延遲時間最短,可是加入任務隊列後仍是會排在Time1和Time2的後面,因此此時按順序執行任務隊列中的代碼,依次打印二、三、5。
因此執行結果爲:
console.log(1); //Time1 setTimeout(function(){ console.log(2); },300); //Time2 setTimeout(function(){ console.log(3) },400); for (var i = 0;i<10000;i++) { console.log(4); } //Time3 setTimeout(function(){ console.log(5); },100);
上面這個問題中,Time3加入任務隊列的時間比Time2,Time1晚,因此它是最後才執行的。那麼問題來了,請看下面代碼:
console.log(1); //Time2 setTimeout(function(){ console.log(3) },400); //Time1 setTimeout(function(){ console.log(2); },300); for (var i = 0;i<10000;i++) { console.log(4); } //Time3 setTimeout(function(){ console.log(5); },100);
咱們將Time1和Time2的順序對換一下,按照前面的說法,Time2先加入任務隊列,而後是Time1,再而後是Time3。但是執行的結果仍是一、四、二、三、5,這是爲何呢?雖然Time1的執行時間短,但是它比Time2晚加入任務隊列啊。
爲了驗證這個問題,咱們能夠提出這樣的一個假設:
若是setTimeout加入隊列的阻塞時間大於兩個setTimeout執行的間隔時間,那麼先加入任務隊列的先執行,儘管它裏面設置的時間比另外一個setTimeout的要大
可能假設聽起來比較拗口,咱們能夠用代碼來理解一下:
代碼1:
//Time2 setTimeout(function(){ console.log(2); },400); var start=new Date(); for (var i = 0;i<5000;i++) { console.log('這裏只是模擬一個耗時操做'); }; var end=new Date(); console.log('阻塞耗時:'+Number(end-start)+'毫秒'); //Time1 setTimeout(function(){ console.log(3) },300);
Time1比Time2設定的執行時間早100ms,可是Time2先加入任務隊列,在Time2和Time1時間有一個阻塞的for循環,執行結果以下:
Time2先執行;
代碼2:
咱們把for循環裏面的時間設置短一點:
setTimeout(function(){ console.log(2); },400); var start=new Date(); for (var i = 0;i<500;i++) { console.log('這裏只是模擬一個耗時操做'); }; var end=new Date(); console.log('阻塞耗時:'+Number(end-start)+'毫秒'); //Time1 setTimeout(function(){ console.log(3) },300);
此時,Time1先執行,由於阻塞的耗時小於Time1和Time2的執行間隔時間100毫秒;
代碼3:
咱們再來驗證一下,把Time2的執行時間設爲350毫秒;
//Time2 setTimeout(function(){ console.log(2); },350); var start=new Date(); for (var i = 0;i<500;i++) { console.log('這裏只是模擬一個耗時操做'); }; var end=new Date(); console.log('阻塞耗時:'+Number(end-start)+'毫秒'); //Time1 setTimeout(function(){ console.log(3) },300);
直接結果爲:
Time2先執行,由於阻塞的時間大於兩個setTimeout之間的間隔時間。
經過上面的假設,咱們能夠得出這樣一個結論:若是setTimeout加入隊列的阻塞時間大於兩個setTimeout執行的間隔時間,那麼先加入任務隊列的先執行,儘管它裏面設置的時間可能比另外一個setTimeout的要大
理解js的事件循環在平時的工做中仍是挺有用的,它可讓咱們清楚的知道事件的執行順序,知道事件的走向,才能更好的駕馭Javascript。本文是對事件循環的一個小小總結,更多的乾貨,能夠看看下面的參考文檔。本文有誤之處,歡迎指出
參看文檔:
【轉向Javascript系列】從setTimeout說事件循環模型