JS運行機制

前言

前端小白學習總結,歡迎討論和指導。html

本文從JS是單線程開始,到JS爲了提升效率,使用異步,到JS如何實現異步,再到瀏覽器是如何配合JS執行異步。最後提到了一個任務隊列的優先級問題。前端

本文是學習JS運行機制中捋出來的思路,有點層層遞進的感受,不免有理解錯誤或表述不當的地方。若是能幫助到看到這篇的你,我很感激。jquery

1、JS是單線程。

所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個。不妨叫它主線程web

選擇單線程的緣由之一是JS要操做DOM,若是多線程可能形成執行混亂。經典栗子來了,有要刪除節點的函數,有要操做節點的。萬一多線程執行順序亂了就壞了。ajax

 

2、JS的異步

單線程使得JS引擎只能一個任務結束再執行下一個,若是某任務時間較長,就會發生阻塞。爲了解決這個問題。JS也使用了異步編程。編程

🍉簡單說下同步(synchronous)和異步(asynchronous)。segmentfault

同步和異步一般是用來形容一個函數被調用時發生的行爲。瀏覽器

同步函數被調用時,調用必須得到預期結果後,才能繼續後續行爲。好比,下面這個(毫無養分的)函數,網絡

var synFunc = function(description){
    var str = "you are";
    str = str + description;
    consoloe.log(str);
}
synFunc("great");  //馬上得到預期結果——在控制檯輸出字符串。

而異步函數被調用時,異步函數的調用會很快完成,異步任務一般會被放到其餘線程中執行。調用者就能夠繼續後續的操做,而沒必要等待這個任務執行完成,才運行。好比,下面這個ajax函數(使用jquery)多線程

$.ajax({
    url:"data.txt";
    async:true; //默認爲true,異步
    success:function(data){
    console.log(data;)
    };
});

運行此函數,讀取文件中數據這個任務,會被放到其餘線程中去執行。等有結果再在控制檯輸出data。在沒得到結果前,後面函數也能夠執行。

🍉 JS的異步實現機制呢,就是咱們在主線程(強調,在JS引擎中負責解釋和執行JavaScript代碼的惟一線程)外,新開一個線程用來執行那些異步任務,咱們暫且稱爲工做線程

具體運行機制能夠理解爲,當主線程的異步函數在被調用的時候,會請求工做線程的幫助。工做線程接收這個任務並執行。主線程能夠繼續運行後面的函數,而沒必要阻塞在這。

🍉 因爲回調函數在JS異步中是個很是重要的概念,咱們先說一下。

異步函數一般具備如下的形式,

var asynFunc = function(args..., callbackFn){
}

asyncFunc能夠叫作異步過程的發起函數,或者叫作異步任務註冊函數。args是這個函數須要的參數。callbackFn是回調函數。回調函數是必須的。

舉個具體的例子:

setTimeout(fn, 1000);

其中的setTimeout就是異步過程的發起函數,fn是回調函數。

注意:前面說的形式A(args..., callbackFn)只是一種抽象的表示,並不表明回調函數必定要做爲發起函數的參數,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回調函數
xhr.open('GET', url);
xhr.send(); // 發起函數

發起函數和回調函數就是分離的。

[文章1]

再好比,事件綁定函數其實也是異步函數。

document.getElementById("btn").addEventLister('click',fucntion(){  
   //...                                           
});

該註冊函數就是異步過程的發起函數,爲click綁定的函數至關於回調函數。

 

3、任務隊列(task queue)和事件循環(event loop)

接下來咱們對JS的異步運行機制加以擴充。異步任務具體是怎麼執行的,主線程和工做線程是怎麼通訊合做的。先上圖。

咱們先解釋兩個概念,同步任務和異步任務。前面咱們提到過的同步函數就是同步任務。異步任務就是異步任務註冊函數觸發的的不在主線程上執行的任務。

主線程中,同步任務組成一個執行棧(execution context stack)。

工做線程執行的異步任務組成一個任務隊列(task queue),也叫事件隊列或消息隊列。只要異步任務有告終果,就將此異步任務的結果(包含回調函數的對象)推入任務隊列中。

主線程完成執行棧中的同步任務後,就會讀取任務隊列,放入主線程中執行。

主線程不斷重複運行執行棧中同步任務,讀取任務隊列,運行異步任務的過程,這就叫事件循環(event loop)。

只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。[文章3]

 

咱們把上面提到的一個有養分的栗子再拉出來遛遛。

$.ajax({
    url:"data.txt";
    async:true; //默認爲true,異步
    success:function(data){
    console.log(data);
    }; //請求成功狀態下的回調函數
});

主線程在發起ajax請求後,會繼續執行其餘代碼。ajax線程負責讀取文檔,拿到響應結果後,把響應封裝成一個JS對象,存放在任務隊列中。

var task = function(data){
   console.log(data); //執行回調函數
}

主線程執行完全部同步任務後,來讀取任務列表,取出此任務放入主線程並執行,即執行回調函數。

 

4、異步操做的執行

前面咱們只是簡單提到主線程外,有工做線程用來執行異步操做。主線程咱們知道,就是JS引擎負責解釋執行JS代碼的惟一線程,也叫JS引擎線程。那工做線程具體指什麼,是什麼執行了那些異步操做呢。

事實上,這些異步操做是由瀏覽器內核的webcore來執行的,webcore包含三種webAPI ,分別是DOM Binding、network、timer模塊。

  • DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發時,回調函數會當即被webcore添加到任務隊列中。

  • network 模塊處理Ajax請求,在網絡請求返回時,纔會將對應的回調函數添加到任務隊列中。

  • timer 模塊會對setTimeout等計時器進行延時處理,當時間到達的時候,纔會將回調函數添加到任務隊列中。

[文章5]

因此,咱們把JS運行機制的圖改成,

 

5、瀏覽器的多線程

既然前面有提到負責JS解釋的線程有且只有一個,咱們叫他主線程,也叫JS引擎線程。也提處處理異步操做的工做是由webAPI負責,那麼剩下的工做過誰作呢,兩者之間的通訊是由什麼執行。接下來咱們將繼續擴充。

事實是,雖然JS是單線程的,但瀏覽器自己是多線程的。

JS運行主要涉及的瀏覽器線程有:

  • JS引擎線程。主線程。負責解析Javascript腳本,運行代碼。一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(renderer進程)中不管何時都只有一個JS線程在運行JS程序。

  • 定時觸發器線程。處理定時器函數setTimeout和setInterval的線程。經過它來計時,在計時完畢後,將任務添加到任務隊列。

  • 事件觸發線程。這個線程和其餘線程不太同樣。是負責線程間的溝通和事件循環的。當JS引擎執行代碼塊如setTimeout時(或者鼠標點擊、ajax請求等),會將任務交給工做線程;當工做線程執行完畢,將結果添加到任務隊列。

  • 異步http請求線程。在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求。將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入任務隊列中。再由JavaScript引擎執行。

固然還有GUI渲染線程等,這裏先不講。繼續補充上圖。

 

6、定時器函數

只說一下setTimeout。前面提到過的栗子。

setTimeout(fn, 1000);

值得注意的就是,第二個參數1000ms,是指執行完這個函數,到將fn推入任務隊列這個動做的時間。並非執行完這個函數到執行fn之間的時間。由於fn推入任務隊列並不必定會被馬上執行。前面提到過,必需要等到執行棧中的同步任務和已在任務隊列中的異步回調函數執行完,纔會執行。

可是咱們一直還有一個問題沒有說,咱們知道主線程中的同步任務必定是按順序執行的,那全部的任務隊列中的任務也是麼。

先看一個題目。

//執行下面這段代碼,執行後,在 5s 內點擊一下,輸出結果是什麼?
setTimeout(function(){
    console.log('timer');
}, 0)
​
function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}
​
document.addEventListener('click', function(){
    console.log('click');
})
​
console.log('click begin');
waitFiveSeconds();

輸出結果是

click begin
finished waiting
click
timer

前面的咱們就不介紹了,只說按理說,setTimeout()函數首先執行,並且延時時間爲0,會馬上被推入任務隊列中,接下來纔是點擊事件的發生和推入。但結果倒是先執行了click函數。這就說明,不一樣類型的任務是有優先級的。

ES5的規範有對這方面的解釋,總結就是:

一個事件循環能夠有多個任務隊列,隊列之間可有不一樣的優先級,同一隊列中的任務按先進先出的順序執行,可是不保證多個任務隊列中的任務優先級,具體實現可能會交叉執行。

具體任務隊列是如何劃分的,詳情可見文章5,不過對咱們的理解沒什麼影響。

相同任務源的任務,只能放到一個任務隊列中。

不一樣任務源的任務,能夠放到不一樣任務隊列中。

此外咱們還能夠很是確定的就是宏任務會先於微任務。不過這裏就不展開了。

 

參考文章
  1. JavaScript:完全理解同步、異步和事件循環(Event Loop)

  2. 同步(Synchronous)和異步(Asynchronous)

  3. JavaScript 運行機制詳解:再談Event Loop

  4. 從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理

  5. JavaScript任務隊列的順序機制(事件循環)

相關文章
相關標籤/搜索