var test = function(){
alert("test");
}
var test2 = function(){
alert("test2");
}
setTimeout(function(){
alert("setTimeout");
},1000);
test();
test2();
//test
//test2
//setTimeout;
上面代碼的運行結果一直讓我很費解,為什么test()
和test2()
沒有在setTimeout()
之后才執行,相當於先執行了定時器外面的函數,而后再執行定時器里的函數,這是為什么呢?在解釋之前,我們有必要知道JavaScript的運行機制。
一、JavaScript為什么是單線程
要回答這個問題,只要我們假設一下,如果JavaScript支持多線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,那么瀏覽器該以哪個線程為准呢?所以答案也就不言而喻了。為了避免復雜性,JavaScript從誕生就是單線程。
在HTML5中,推出了web worker標准,允許JavaScript腳本創建多個線程,但是子線程完全受主線程的控制,且不得操作DOM,所以也是沒有違背JavaScript單線程的本質。
二、任務隊列
因為JavaScript是單線程,意味着任務要一個接着一個完成,但是,如果前一個任務執行時間很長,那么后面的任務就得一直阻塞着,這樣用戶體驗十分差。
JavaScript的設計者考慮到了這一點,所以他將JavaScript的任務分為兩種,在主線程上執行的任務"同步任務"
,被主線程掛載起來的任務"異步任務"
,后者一般是放在一個叫任務隊列
的數據結構中。
那么一般異步執行運行機制如下(也是JavaScript的運行機制):
- (1)所有同步任務都在主線程上執行,形成一個執行棧。
- (2)主線程之外,還有一個“任務隊列”,只要異步任務有了運行結果,就在“任務隊列”之中放置一個事件。
- (3)一旦“執行棧”中的所有同步任務執行完畢了,系統就會讀取“任務隊列”,看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
- (4)主線程不斷重復上面的三步。(事件循環)
參考圖片(圖片來源 阮一峰老師講解的JavaScript運行機制詳解)
三、事件和回調函數
看了前面的講解,我一開始提出的問題視乎可以這樣理解,test()
和test2()
屬於“執行棧”中的同步任務,而定時器
則是任務隊列里面的異步任務,那么定時器就是屬於異步任務中的一種,在講定時器之前先認識一下任務隊列里面的另外一個重要成員,事件
。其實任務隊列就是一個事件隊列,因為一般我們綁定一個事件,比如點擊事件等等,都是在某一個時刻才觸發執行的,這個時候就得放到任務隊列里面,等待執行,而在某個DOM節點上綁定了事件,就要有相應的回調函數
,它們是相輔相成的。
所謂回調函數
,就是那些被掛載起來,等待執行的代碼,主線程執行任務隊列里面的異步任務,其實就是執行這些回調函數。
一般只有主線程所有任務都執行完畢了,才會執行任務隊列里面的異步任務,一般是按照隊列的“先進先出”順序執行,但是因為存在定時器
,所以主線程要檢查執行時間,只有到了規定的時間,才能返回主線程。
四、定時器
終於到特殊的定時器了,定時器主要由setTimeout()
和setInterval()
兩個函數來完成,它們的內部運行機制完全一樣,不同的只是,前者一次性執行,而后者反復執行。定時器,屬於任務隊列中的異步任務,所以才會出現上面的問題,再看幾個例子就能理解了,
console.log(1);
setTimeout(function(){ console.log(2);},1000);
console.log(3);
上面代碼的執行結果是1,3,2,因為只有setTimeout里面的代碼是異步任務,其它都是主線程里的同步任務,所以只有執行完了主線程中的所有任務,才會執行setTimeout中的任務。