淺談js執行機制
關於js執行機制,老早之前就一直想寫篇文章做個總結,因為和js執行順序的面試題碰到的特別多,每次碰到總是會去網上查,沒有系統地總結,搞得每次碰到都是似懂非懂的感覺,這篇文章就係統的總結一下js執行機制。
任務佇列
大家都知道js最大的特點就是單執行緒執行,這就是為什麼js簡單易學的一個重要原因,不需要考慮複雜的同步問題,但是單執行緒也會有一個問題,所有的任務在執行的過程中都必須等待前一個任務執行完成才能執行,這樣就會帶來一個效率的問題,為了解決這個問題,js將任務分為兩種:同步任務和非同步任務,同步任務就是之前說後一個任務必須等待前一個任務執行完成才能執行,是在主執行緒上執行的,而非同步任務不會直接進入主執行緒執行,而是進入任務佇列,只有在任務佇列通知非同步任務可以執行時,才會被推入主執行緒執行。讓我們來看一個更加直觀的流程圖:
clipboard.png
setTimeout和setInterval
說到非同步任務,最常見就是setTimeout和setInterval兩兄弟了,setTimeout是延遲一定時間後執行,但是隻執行一次,setInterval是每隔一定的時間執行一次,會執行多次,但是有時候我們會發現設定一定的延遲時間後,回撥函式的執行時間會比我們設定的時間要晚,這是為什麼呢?上面我們說過,在任務執行的時候setTimeout這類非同步任務的回撥會被放到非同步佇列中等待執行,當延遲時間結束時,如果主執行緒的任務已經執行完了,也就是處在空閒狀態時,就會將任務佇列的回撥推到主執行緒執行,但是當主執行緒的任務還沒有執行完成時,就只能繼續等待,來看一個例子:
let before = new Date()
setTimeout(() => {
console.log(new Date() - before)
}, 1000)
for (let i = 0; i < 300000; i++) {
console.log(‘time delay’)
}
從上面的例子就可以看到:當我們執行完setTimeout之後,立刻執行20萬次的迴圈,從執行結果可以看到,setTimeout回撥函式中的時間遠高於設定1000ms,這就是因為時間到了,但是主執行緒的任務還沒有執行完成導致。這種問題在setInterval設定倒計時的經常遇到,倒計時開始的時候設定的時間是從伺服器拿到的系統時間很準確,但是如果後面不定期像服務期請求系統時間進行校準的話,你可能會發現倒計時的偏差越來越來大,這就是主執行緒執行的時間比設定的延遲時間長導致的。
macrotask和microtask
在js中,非同步任務除了有setTimeout這類的非同步任務,還有一類就是es6中很常用promise…then這類的非同步任務,因此除了同步任務和非同步任務,任務還可以更加細分為macrotask(巨集任務)和microtask(微任務)
macrotask: 包括setTimeout、setInterval和執行棧
microtask: 包括Promise、process.nextTick
要想理解這兩個概念,直接從一道簡單的面試題入手,來看一個例子:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
思考一下上面例子的輸出結果,我們來仔細分析一下執行過程:
第一輪:主執行緒開始執行,遇到setTimeout,將setTimeout的回撥函式丟到巨集任務佇列中,在往下執行new Promise立即執行,輸出2,then的回撥函式丟到微任務佇列中,再繼續執行,遇到process.nextTick,同樣將回調函式扔到為任務佇列,再繼續執行,輸出5,當所有巨集任務執行完成後看有沒有可以執行的微任務,發現有then函式和nextTick兩個微任務,先執行哪個呢?process.nextTick指定的非同步任務總是發生在所有非同步任務之前,因此先執行process.nextTick輸出4然後執行then函式輸出3,第一輪執行結束。
第二輪從巨集任務佇列開始,發現setTimeout回撥,輸出1執行完畢,因此結果是25431
最後用一張圖來總結一下:
clipboard.png