JS單線程與setTimeout執行原理

Javascript 引擎單線程機制

  • 首先明確,JavaScript引擎是單線程機制。
  • JavaScript 是單線程執行的,無法同時執行多段代碼。當某一段代碼正在執行的時候,所有後續的任務都必須等待,形成一個任務隊列。一旦當前任務執行完畢,再從隊列中取出下一個任務,這也常被稱爲 “阻塞式執行”。
  • 可以理解爲:只有在JS線程中沒有任何同步代碼要執行的前提下才會執行異步代碼

  • 所以一次鼠標點擊,或是計時器到達時間點,或是 Ajax 請求完成觸發了回調函數,這些事件處理程序或回調函數都不會立即運行,而是立即排隊,一旦線程有空閒就 執行。假如當前 JavaScript 線程正在執行一段很耗時的代碼,此時發生了一次鼠標點擊,那麼事件處理程序就被阻塞,用戶也無法立即看到反饋,事件處理程序會被放入任務隊列,直到前面的代碼結束以後纔會開始執行。如果代碼中設定了一個 setTimeout,那麼瀏覽器便會在合適的時間,將代碼插入任務隊列,如果這個時間設爲 0,就代表立即插入隊列,但不是立即執行,仍然要等待前面代碼執行完畢。所以 setTimeout 並不能保證執行的時間,是否及時執行取決於 JavaScript 線程是擁擠還是空閒。

瀏覽器的多線程機制與事件循環(event loop)

  • 首先明確,瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:

    • javascript 引擎線程
    • GUI 渲染線程
    • 瀏覽器事件觸發線程
  • JavaScript 引擎是單線程運行的,瀏覽器無論在什麼時候都只且只有一個線程在運行JavaScript程序

    • javascript 引擎是基於事件驅動單線程執行的,JS引擎一直等待着任務隊列中任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JS線程在運行JS程序。
  • GUI渲染線程負責渲染瀏覽器界面,當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行。但需要注意 GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行。
  • 事件觸發線程,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可來自 JavaScript 引擎當前執行的代碼塊如 setTimeOut,也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX 異步請求等,但由於JS的單線程關係所有這些事件都得排隊等待JS引擎處理。(當線程中沒有執行任何同步代碼的前提下才會執行異步代碼)
  • 事件循環(event loop): 是用來管理我們的異步代碼的,它會把它們放在一個線程池當中

JavaScript中setTimeout的實現原理

  • 首先明確,setTimeout函數是異步代碼,但其實setTimeout並不是真正的異步操作
  • 由於JS線程的工作機制:當線程中沒有執行任何同步代碼的前提下才會執行異步代碼,setTimeout是異步代碼,所以setTimeout只能等js空閒纔會執行
  • 前面提到過,如果代碼中設定了一個 setTimeout,那麼瀏覽器便會在合適的時間,將代碼插入任務隊列,如果這個時間設爲 0,就代表立即插入隊列,但不是立即執行,仍然要等待前面代碼執行完畢。所以 setTimeout 並不能保證執行的時間,是否及時執行取決於 JavaScript 線程是擁擠還是空閒。
  • 也就是說setTimeout只能保證在指定的時間過後將任務(需要執行的函數)插入隊列等候,並不保證這個任務在什麼時候執行。執行javascript的線程會在空閒的時候,自行從隊列中取出任務然後執行它。javascript 通過這種隊列機制,給我們製造一個異步執行的假象。
  • 有時setTimeout中的代碼會很快得到執行,我們會感覺這段代碼是在異步執行,這是因爲 javascript 線程並沒有因爲什麼耗時操作而阻塞,所以可以很快地取出排隊隊列中的任務然後執行它。

實例分析

在具備了上述理論基礎之後,我們對以下幾個實例進行分析:

  1. ===========================================

    var t = true;
        
    window.setTimeout(function (){
        t = false;
    },1000);
        
    while (t){}
        
    alert('end');

    運行結果:程序陷入死循環,t = false 得不到執行,因此 alert('end') 不會執行。
    解析:

    • JS是單線程的,所以會先執行 while(t){} 再 alert,但這個循環體是死循環,所以永遠不會執行alert。
    • 爲什麼不執行 setTimeout?是因爲JS的工作機制是:當線程中沒有執行任何同步代碼的前提下才會執行異步代碼,setTimeout是異步代碼,所以 setTimeout 只能等JS空閒纔會執行,但死循環是永遠不會空閒的,所以 setTimeout 也永遠得不到執行。
  2. ===========================================

    var start = new Date();
        
    setTimeout(function(){  
        var end = new Date();  
        console.log("Time elapsed: ", end - start, "ms");  
    }, 500);  
          
    while (new Date - start <= 1000){}

    運行結果:"Time elapsed: 1035 ms" (這裏的1035不準確 但是一定是大於1000的)
    解析:

    • JS是單線程 setTimeout 異步代碼 其回調函數執行必須需等待主線程運行完畢
    • 當while循環因爲時間差超過 1000ms 跳出循環後,setTimeout 函數中的回調才得以執行
  3. ===========================================

    for(var i=0;i<10;i++){
        setTimeout(function() {
            console.log(i);
        }, 0);
    }
    

    運行結果:輸出10個10
    解析:JS單線程 setTimeout 異步代碼 任務隊列
    問:如何修改可以使上述代碼輸出 0123456789
    自執行函數 或 使用ES6中的let關鍵字

    // 自執行函數 形成閉包 記憶其被創建時的環境
    for(var i=0;i<10;i++){
        setTimeout((function() {
             console.log(i);
        })(), 0);
    }

setTimeout(0)函數的作用

現在我們瞭解了setTimeout函數執行的原理,那麼它有什麼作用呢?
setTimeout函數增加了Javascript函數調用的靈活性,爲函數執行順序的調度提供極大便利。
簡言之,改變順序,這正是setTimeout(0)的作用。

使用場景示例:

<input type="text" onkeydown="show(this.value)">  
<div></div>  
<script type="text/javascript">  
  function show(val) {  
    document.getElementsByTagName('div')[0].innerHTML = val;  
  }  
</script> 

這裏綁定了 keydown 事件,意圖是當用戶在文本框裏輸入字符時,將輸入的內容實時地在 <div> 中顯示出來。但是實際效果並非如此,可以發現,每按下一個字符時,<div> 中只能顯示出之前的內容,無法得到當前的字符。

修改代碼:

  <input type="text" onkeydown="var self=this; setTimeout(function(){show(self.value)}, 0)">  
  <div></div>  
  <script type="text/javascript">  
    function show(val) {  
      document.getElementsByTagName('div')[0].innerHTML = val;  
    }  
  </script>

這段代碼使用setTimeout(0)就可以實現需要的效果了。

這裏其實涉及2個任務,1個是將鍵盤輸入的字符回寫到輸入框中,一個是獲取文本框的值將其寫入div中。第一個是瀏覽器自身的默認行爲,一個是我們自己編寫的代碼。很顯然,必須要先讓瀏覽器將字符回寫到文本框,然後我們才能獲取其內容寫到div中。改變順序,這正是setTimeout(0)的作用。

其他應用場景:有時候,加載一些廣告的時候,我們用setTimeout實現異步,好讓廣告不會阻塞我們頁面的渲染。

setTimeout 和 setInterval 在執行異步代碼的時候有着根本的不同

  • 如果一個計時器被阻塞而不能立即執行,它將延遲執行直到下一次可能執行的時間點才被執行(比期望的時間間隔要長些)
  • 如果setInterval回調函數的執行時間將足夠長(比指定的時間間隔長),它們將連續執行並且彼此之間沒有時間間隔。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章