1. 程式人生 > >js的單執行緒和瀏覽器的多執行緒

js的單執行緒和瀏覽器的多執行緒

js執行是單執行緒:(傳送請求,接受請求,渲染頁面,執行js等等這些就是一個個執行緒。)

JS引擎

通常講到瀏覽器的時候,我們會說到兩個引擎:渲染引擎和JS引擎。渲染引擎就是如何渲染頁面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎對同一個樣式的實現不一致,就導致了經常被人詬病的瀏覽器樣式相容性問題。這裡我們不做具體討論。

JS引擎可以說是JS虛擬機器,負責JS程式碼的解析和執行。通常包括以下幾個步驟:

  • 詞法分析:將原始碼分解為有意義的分詞
  • 語法分析:用語法分析器將分詞解析成語法樹
  • 程式碼生成:生成機器能執行的程式碼
  • 程式碼執行

不同瀏覽器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

之所以說JavaScript是單執行緒,就是因為瀏覽器在執行時只開啟了一個JS引擎執行緒來解析和執行JS。那為什麼只有一個引擎呢?如果同時有兩個執行緒去操作DOM,瀏覽器是不是又要不知所措了。

所以,雖然JavaScript是單執行緒的,可是瀏覽器內部不是單執行緒的。一些I/O操作、定時器的計時和事件監聽(click, keydown...)等都是由瀏覽器提供的其他執行緒來完成的。

一個瀏覽器通常由以下幾個常駐的執行緒:

  • 渲染引擎執行緒:顧名思義,該執行緒負責頁面的渲染
  • JS引擎執行緒:負責JS的解析和執行
  • 定時觸發器執行緒:處理定時事件,比如setTimeout, setInterval
  • 事件觸發執行緒:處理DOM事件
  • 非同步http請求執行緒:處理http請求

需要注意的是,渲染執行緒和JS引擎執行緒是不能同時進行的。渲染執行緒在執行任務的時候,JS引擎執行緒會被掛起。因為JS可以操作DOM,若在渲染中JS處理了DOM,瀏覽器可能就不知所措了。

同步執行和非同步執行

  1. 同步:只有前一個任務執行完畢,才能執行後一個任務
  2. 非同步:當同步任務執行到某個 WebAPI 時,就會觸發非同步操作,此時瀏覽器會單獨開執行緒去處理這些非同步任務。

任務佇列、回撥佇列、事件迴圈

WebAPI 是啥?瀏覽器事件、定時器、ajax,這些操作不會阻塞 JS 的執行,JS 會跳過當前程式碼,執行後續程式碼

  • 任務佇列( Task Queue ):主執行緒執行完畢後所觸發的非同步任務( WebAPIs ),叫任務佇列
  • 回撥佇列( Callback Queue ):這些非同步 WebAPI 執行完成後得到的結果,會新增到 callback queue
  • 事件迴圈( Event Loop ):只要主執行緒的同步任務執行完畢,就會不斷的讀取 "回撥佇列" 中的回撥函式,到主執行緒中執行,這個過程不斷迴圈往復

如何知道主執行緒執行執行完畢?JS引擎存在 monitoring process 程序,會持續不斷的檢查主執行緒執行為空,一旦為空,就會去 callback queue 中檢查是否有等待被呼叫的函式。

console.log('1');
setTimeout(function() {
    console.log('2');
}, 0);
console.log('3');
  • 列印1
  • 遇到 WebAPI( setTimeout ) ,瀏覽器新開定時器執行緒處理,執行完成後把回撥函式存放到回撥佇列中。專業一點的說發: JS 引擎遇到非同步任務後不會一直等待其返回結果,而是將這個任務掛起交給其他瀏覽器執行緒處理,自己繼續執行主執行緒中的其他任務。這個非同步任務執行完畢後,把結果返回給回撥佇列。被放入的程式碼不會被立即執行。而是當主執行緒所有同步任務執行完畢, monitoring process 程序就會把 "回撥佇列" 中的第一個回撥程式碼放入主執行緒。然後主執行緒執行程式碼。如此反覆
  • 列印3 非同步 setTimeout 不會阻塞同步程式碼,因此會首先列印3
  • 主執行緒執行完畢後,執行 Callback Queue 列印2

非同步任務的執行優先順序並不相同,它們被分為兩類:微任務( micro task ) 和 巨集任務( macro task ) 根據非同步事件的型別,這些事件實際上會被派發對應的巨集任務和微任務中,在當前主執行緒執行完畢後,

  1. 會先檢視微任務中是否有事件存在,如果不存在,則再去找巨集任務
  2. 如果存在,則會依次執行佇列中的引數,直到微任務列表為空,讓後去巨集任務中一次讀取事件到主執行緒中執行,如此反覆 當前主執行緒執行完畢後,會首先處理微任務佇列中的事件,讓後再去讀取巨集任務佇列的事件。在同一次事件迴圈中,微任務永遠在巨集任務之前執行。
  1. 巨集任務( macro-task ):整體 scriptsetTimeoutsetIntervalUI互動事件I/O
  2. 微任務( micro-task ):process.nextTickPromiseMutaionObserver
(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function (resolve, reject) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
1. setTimeout:巨集任務:存入巨集任務佇列
2. Promise:函式本身是同步執行的( **Promise** 只有一個引數,預設new的時候就會同步執行), `.then` 是非同步,因此依次列印1、2  `.then` 存入微任務中
3. 列印3( 第一次主執行緒執行完畢 )
4. 執行微任務中的回撥函式:5, 讓後執行巨集任務中的 `setTimeout` 4
// 最終結果1,2,3,5,4