JS運行機制


前言

本文從JS是單線程開始,到JS為了提高效率,使用異步,到JS如何實現異步(做法是主線程外另開工作線程和任務隊列,他們之間是如何工作的——事件循環),再到瀏覽器是如何配合JS執行異
步(其他瀏覽器線程)。最后提到了一個任務隊列的優先級問題。
涉及的需要重點理解的概念有主線程、執行棧、異步、異步任務、任務隊列、事件循環等。

 

一、JS是單線程。

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

選擇單線程的原因之一是JS要操作DOM,如果多線程可能造成執行混亂。經典栗子來了,有要刪除節點的函數,有要操作節點的。萬一多線程執行順序亂了就壞了。

 

二、JS的異步

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

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

同步和異步通常是用來形容一個函數被調用時發生的行為。

同步函數被調用時,調用必須獲得預期結果后,才能繼續后續行為。比如,下面這個(毫無營養的)函數,

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綁定的函數相當於回調函數。

 

三、任務隊列(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); //執行回調函數
}

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

 

四、異步操作的執行

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

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

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

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

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

[文章5]

所以,我們把JS運行機制的圖改為,

 

 

五、瀏覽器的多線程

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

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

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

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

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

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

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

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

 

六、定時器函數

只說一下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,不過對我們的理解沒什么影響。

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

不同任務源的任務,可以放到不同任務隊列中。

此外我們還可以非常肯定的就是宏任務會先於微任務。不過這里就不展開了。

 

總結

瀏覽器中,解釋和運行JavaScrip代碼的線程只有一個,我們稱其為主線程。單線程有它的必要性,可以避免操作DOM的時候產生混亂,但也有明顯的缺點,很容易造成阻塞。所以JS同樣使用了異步編程。具體操作是,把要執行的任務分為兩類,同步任務和異步任務。同步任務一般是那些可以立刻拿到結果的任務,這類任務直接放在主線程中執行,形成執行棧。常見的異步任務有事件綁定函數、ajax請求、定時器瀏覽器和具有回調函數的函數等,這類任務瀏覽器會另開線程執行,執行完的任務結果,一般是指返回回調函數的一個對象,它們會被放在任務隊列中。當主線程執行完執行棧中的同步任務,就會來任務隊列中取異步任務結果,並執行它們。這個過程會一直反復,我們叫它事件循環。整個JS的運行機制大概就是這樣。

本文是小白學習JS運行機制中捋出來的思路,有點層層遞進的感覺,難免有理解錯誤或表述不當的地方,請指正,謝謝。另外,如果能幫助到看到這篇的你,我很感激。

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

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

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM