JavaScript是單線程異步執行的,單線程意味着代碼在任務隊列中會按照順序一個接一個的執行。異步表明JavaScript代碼在任務隊列中的順序並不徹底等同於代碼的書寫順序,好比事件綁定、Ajax、setTimeout()等任務的發生時間是「不可被預期」的。html
既然JavaScript是單線程機制,那Ajax爲何是異步的?setTimeout()是怎樣執行的?promise
在瀏覽器中,JavaScript引擎是單線程執行的。也就是說,在同一時間內,只能有一段代碼被JavaScript引擎執行。頁面加載時,JavaScript引擎會順序執行頁面上全部JavaScript代碼,優先執行同步代碼。而異步代碼由事件觸發引擎按照「事件發生」的順序添加到JavaScript引擎的任務隊列中,待全部同步代碼執行結束後,JavaScript引擎會按照任務隊列中的順序來執行異步代碼。瀏覽器
下面是知乎上的一段回答:多線程
JavaScript引擎是單線程運行的,瀏覽器不管在何時都只且只有一個線程在運行JavaScript程序。異步
瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:JavaScript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。線程
- JavaScript引擎是基於事件驅動單線程執行的,JavaScript引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JavaScript線程在運行JavaScript程序。
- GUI渲染線程負責渲染瀏覽器界面,當界面須要重繪(Repaint)或因爲某種操做引起迴流(Reflow)時,該線程就會執行。但須要注意,GUI渲染線程與JavaScript引擎是互斥的,當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JavaScript引擎空閒時當即被執行。
- 事件觸發線程,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JavaScript引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeout、也可來自瀏覽器內核的其餘線程如鼠標點擊、Ajax異步請求等,但因爲JavaScript的單線程關係全部這些事件都得排隊等待JavaScript引擎處理(當線程中沒有執行任何同步代碼的前提下才會執行異步代碼)。
瞭解JavaScript單線程異步執行的機制之後,再來看一看setTimeout()與setInterval()在執行時候的具體狀況。htm
JavaScript引擎在執行setTimeout(fn, 10)時,一方面繼續執行setTimeout(fn, 10)後面的同步代碼,同時另外一方面開始計時,在10ms以後將fn插入任務隊列中。待全部同步代碼執行結束後(JavaScript引擎空閒),依次任務隊列中的異步代碼。因此,setTimeout(fn, 10)並不能準確的在10ms以後執行,而是大於等於10ms。blog
看下面兩段代碼,會對setTimeout()的執行順序有更直觀的印象。隊列
第一段:事件
console.log(1) setTimeout(function () {console.log('a')}, 10); setTimeout(function () {console.log('b')}, 0); var sum = 0; for (var i = 0; i < 1000000; i ++) { sum += i; } console.log(sum); setTimeout(function () {console.log('c');}, 0);
輸出結果:
代碼執行的邏輯如圖所示,縱向表明時間,左邊表示同步代碼的執行順序,右邊表示異步代碼的任務隊列,從左到後的箭頭表示將異步代碼插入任務隊列。
第二段,將for循環上限去掉一個0:
console.log(1) setTimeout(function () {console.log('a')}, 10); setTimeout(function () {console.log('b')}, 0); var sum = 0; for (var i = 0; i < 100000; i ++) { sum += i; } console.log(sum); setTimeout(function () {console.log('c');}, 0);
輸出結果:
兩段代碼的區別在於for循環執行的時間不一樣,第一段代碼的for循環執行時間大於10ms,因此console.log('a')先被插入任務隊列,等for循環執行結束後,console.log('c')才被插入任務隊列。第二段代碼的for循環執行時間小於10ms,因此console.log('c')先被插入任務隊列。
setInterval()的執行方式與setTimeout()有不一樣。假如執行setInterval(fn, 10),則每隔10ms,定時器的事件就會被觸發。與setTimeout()相同的是,若是當前沒有同步代碼在執行(JavaScript引擎空閒),則定時器對應的方法fn會被當即執行,不然,fn就會被加入到任務隊列中。因爲定時器的事件是每隔10ms就觸發一次,有可能某一次事件觸發的時候,上一次事件的處理方法fn尚未機會獲得執行,仍然在等待隊列中,這個時候,這個新的定時器事件就被丟棄,繼續開始下一次計時。須要注意的是,因爲JavaScript引擎這種單線程異步的執行方式,有可能兩次fn的實際執行時間間隔小於設定的時間間隔。好比上一個定時器事件的處理方法觸發以後,等待了5ms纔得到被執行的機會。而第二個定時器事件的處理方法被觸發以後,立刻就被執行了。那麼這二者之間的時間間隔實際上只有5ms。所以,setInterval()並不適合實現精確的按固定間隔的調度操做。
下面代碼說明了這個問題:
console.log(1) var interval = setInterval(function () { var date = new Date(); console.log(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); }, 10); var sum = 0; for (var i = 0; i < 1000000; i ++) { sum += i; } console.log(2); // 清除定時器,避免卡死瀏覽器 setTimeout(function () { clearInterval(interval); }, 100);
輸出結果:
能夠看出,setInterval()前兩次的間隔時間只有4ms。由於setInterval()第一次被觸發後,裏面的方法並無立刻被執行,而是等待同步代碼執行結束後才被執行,這個過程用了6ms。因此當第一次方法執行事後4ms,第二次方法也被執行了。從setInterval()第二次被觸發開始,後面幾回的執行都沒有被阻塞,因此間隔時間都在11ms左右。
總的來講,setTimeout()和setInterval()都不能知足精確的時間間隔。假如設定的時間間隔爲10ms,則setTimeout(fn, 10)中的fn執行的時間間隔可能大於10ms,而setInterval(fn, 10)中fn執行的時間間隔可能小於10ms。