深入前端-徹底搞懂JS的運行機制

最近看了很多關於JS運行機制的文章,每篇都獲益匪淺,但各有不同,所以在這裏對這幾篇文章裏說的很精闢的地方做一個總結,參考文章鏈接見最後。本文博客地址

瞭解進程和線程

  • 進程是應用程序的執行實例,每一個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程中能夠申請創建和使用系統資源(如- 獨立的內存區域等),這些資源也會隨着進程的終止而被銷燬。
  • 而線程則是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的,所以在多線程的情況下,需要特別注意對臨界資源的訪問控制。
  • 在系統創建進程之後就開始啓動執行進程的主線程,而進程的生命週期和這個主線程的生命週期一致,主線程的退出也就意味着進程的終止和銷燬。
  • 主線程是由系統進程所創建的,同時用戶也可以自主創建其它線程,這一系列的線程都會併發地運行於同一個進程中。

瀏覽器是多進程的

詳情看我上篇總結瀏覽器執行機制的文章-深入前端-徹底搞懂瀏覽器運行機制
  • 瀏覽器每打開一個標籤頁,就相當於創建了一個獨立的瀏覽器進程。
  • Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有
  • 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
  • GPU進程:最多一個,用於3D繪製等
  • 瀏覽器渲染進程(瀏覽器內核)

javascript是一門單線程語言

  • jS運行在瀏覽器中,是單線程的,但每個tab標籤頁都是一個進程,都含有不同JS線程分別執行,,一個Tab頁(renderer進程)中無論什麼時候都只有一個JS線程在運行JS程序
  • 既然是單線程的,在某個特定的時刻只有特定的代碼能夠被執行,並阻塞其它的代碼。而瀏覽器是事件驅動的(Event driven),瀏覽器中很多行爲是異步(Asynchronized)的,會創建事件並放入執行隊列中。javascript引擎是單線程處理它的任務隊列,你可以理解成就是普通函數和回調函數構成的隊列。當異步事件發生時,如(鼠標點擊事件發生、定時器觸發事件發生、XMLHttpRequest完成回調觸發等),將他們放入執行隊列,等待當前代碼執行完成。
  • javascript引擎是基於事件驅動單線程執行的,JS引擎一直等待着任務隊列中任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JS線程在運行JS程序。所以一切javascript版的"多線程"都是用單線程模擬出來的
  • 爲什麼JavaScript是單線程?與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?

任務隊列

  • "任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。
  • "任務隊列"中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等),ajax請求等。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。
  • 所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
  • "任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由於存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

同步和異步任務

既然js是單線程,那麼問題來了,某一些非常耗時間的任務就會導致阻塞,難道必須等前面的任務一步一步執行玩嗎?
比如我再排隊就餐,前面很長的隊列,我一直在那裏等豈不是很傻逼,說以就會有排號系統產生,我們訂餐後給我們一個號碼,叫到號碼直接去就行了,沒交我們之前我們可以去幹其他的事情。
因此聰明的程序員將任務分爲兩類:

  • 同步任務:同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
  • 異步任務:異步任務指的是,不進入主線程、而進入"任務隊列"(Event queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。

任務有更精細的定義:

  • macro-task(宏任務):包括整體代碼script(同步宏任務),setTimeout、setInterval(異步宏任務)
  • micro-task(微任務):Promise,process.nextTick,ajax請求(異步微任務)

macrotask(又稱之爲宏任務)

可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
每一個task會從頭到尾將這個任務執行完畢,不會執行其它
瀏覽器爲了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染
(task->渲染->task->...)

microtask(又稱爲微任務),可以理解是在當前 task 執行結束後立即執行的任務

也就是說,在當前task任務後,下一個task之前,在渲染之前
所以它的響應速度相比setTimeout(setTimeout是task)會更快,因爲無需等渲染
也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

執行機制與事件循環

主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

那怎麼知道主線程執行棧爲執行完畢?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

第一輪事件循環:
主線程執行js整段代碼(宏任務),將ajax、setTimeout、promise等回調函數註冊到Event Queue,並區分宏任務和微任務。
主線程提取並執行Event Queue 中的ajax、promise等所有微任務,並註冊微任務中的異步任務到Event Queue。
第二輪事件循環:
主線程提取Event Queue 中的第一個宏任務(通常是setTimeout)。
主線程執行setTimeout宏任務,並註冊setTimeout代碼中的異步任務到Event Queue(如果有)。
執行Event Queue中的所有微任務,並註冊微任務中的異步任務到Event Queue(如果有)。
類似的循環:宏任務每執行完一個,就清空一次事件隊列中的微任務。

注意:事件隊列中分“宏任務隊列”和“微任務隊列”,每執行一次任務都可能註冊新的宏任務或微任務到相應的任務隊列中,只要遵循“每執行一個宏任務,就會清空一次事件隊列中的所有微任務”這一循環規則,就不會弄亂。

說了那麼多來點實例吧

ajax普通異步請求實例

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('發送成功!');
    }
})
console.log('代碼執行結束');

1.執行整個代碼,遇到ajax異步操作
2.ajax進入Event Table,註冊回調函數success。
3.執行console.log('代碼執行結束')。
4.執行ajax異步操作
5.ajax事件完成,回調函數success進入Event Queue。
5.主線程從Event Queue讀取回調函數success並執行。

普通微任務宏任務實例

setTimeout(function(){
    console.log('定時器開始啦')
});

new Promise(function(resolve){
    console.log('馬上執行for循環啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('執行then函數啦')
});

console.log('代碼執行結束');

1.整段代碼作爲宏任務執行,遇到setTimeout宏任務分配到宏任務Event Queue中
2.遇到promise內部爲同步方法直接執行-“馬上執行for循環啦”
3.註冊then回調到Eventqueen
4.主代碼宏任務執行完畢-“代碼執行結束”
5.主代碼宏任務結束被monitoring process進程監聽到,主任務執行Event Queue的微任務
6.微任務執行完畢-“執行then函數啦”
7.執行宏任務console.log('定時器開始啦')

地獄模式:promise和settimeout事件循環實例

console.log('1');
// 1 6 7 2 4 5 9 10 11 8 3
// 記作 set1
setTimeout(function () {
    console.log('2');
    // set4
    setTimeout(function() {
        console.log('3');
    });
    // pro2
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})

// 記作 pro1
new Promise(function (resolve) {
    console.log('6');
    resolve();
}).then(function () {
    console.log('7');
    // set3
    setTimeout(function() {
        console.log('8');
    });
})

// 記作 set2
setTimeout(function () {
    console.log('9');
    // 記作 pro3
    new Promise(function (resolve) {
        console.log('10');
        resolve();
    }).then(function () {
        console.log('11');
    })
})

第一輪事件循環

1.整體script作爲第一個宏任務進入主線程,遇到console.log,輸出1。

2.遇到set1,其回調函數被分發到宏任務Event Queue中。

3.遇到pro1,new Promise直接執行,輸出6。then被分發到微任務Event Queue中。

4.遇到了set2,其回調函數被分發到宏任務Event Queue中。

  1. 主線程的整段js代碼(宏任務)執行完,開始清空所有微任務;主線程執行微任務pro1,輸出7;遇到set3,註冊回調函數。

第二輪事件循環

1.主線程執行隊列中第一個宏任務set1,輸出2;代碼中遇到了set4,註冊回調;又遇到了pro2,new promise()直接執行輸出4,並註冊回調;

2.set1宏任務執行完畢,開始清空微任務,主線程執行微任務pro2,輸出5。

第三輪事件循環

1.主線程執行隊列中第一個宏任務set2,輸出9;代碼中遇到了pro3,new promise()直接輸出10,並註冊回調;

2.set2宏任務執行完畢,開始情況微任務,主線程執行微任務pro3,輸出11。

類似循環...

所以最後輸出結果爲1、6、7、2、4、5、9、10、11、8、3。

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章