深刻前端-完全搞懂JS的運行機制

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

CPU、進程、線程之間的關係

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)
  • 不一樣進程之間也能夠通訊,不過代價較大
  • 單線程多線程,都是指在一個進程內的單和多

瞭解進程和線程

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

瀏覽器是多進程的

詳情看我上篇總結瀏覽器執行機制的文章- 深刻前端-完全搞懂瀏覽器運行機制
  • 瀏覽器每打開一個標籤頁,就至關於建立了一個獨立的瀏覽器進程。
  • 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是單線程,那麼問題來了,某一些很是耗時間的任務就會致使阻塞,難道必須等前面的任務一步一步執行玩嗎?
好比我再排隊就餐,前面很長的隊列,我一直在那裏等豈不是很傻逼,說以就會有排號系統產生,咱們訂餐後給咱們一個號碼,叫到號碼直接去就好了,沒交咱們以前咱們能夠去幹其餘的事情。
所以聰明的程序員將任務分爲兩類:javascript

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

任務有更精細的定義:

  • macro-task(宏任務):包括總體代碼script(同步宏任務),setTimeout、setInterval(異步宏任務)、I/O、UI 交互事件(優先級較高)、postMessage、MessageChannel、setImmediate(Node.js 環境)。
  • micro-task(微任務):Promise,process.nextTick,ajax請求(異步微任務)

macrotask(又稱之爲宏任務)

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

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

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

執行機制與事件循環

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

那怎麼知道主線程執行棧爲執行完畢?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

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

說了那麼多來點實例吧

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並執行。瀏覽器

Promise 的鏈式 then() 是怎樣執行的

new Promise((r) => {
    r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
    r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

執行的結果是 1 4 2 5 3 6數據結構

  • Promise 多個then()鏈式調用,並非連續的建立了多個微任務並推入微任務隊列,由於then()的返回值必然是一個 Promise,然後續的then()是上一步then()返回的 Promise 的回調
  • 傳入 Promise 構造器的執行器函數內部的同步代碼執行到resolve(),將 Promise 的狀態改變爲<resolved>: undefined, 而後 then 中傳入的回調函數console.log('1')做爲一個微任務被推入微任務隊列
  • 第二個then()中傳入的回調函數console.log('2')此時尚未被推入微任務隊列,只有上一個then()中的console.log('1')執行完畢後,console.log('2')纔會被推入微任務隊列

普通微任務宏任務實例

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('定時器開始啦')多線程

async/await執行順序

// 1 2 6 4 3 5
//console.log(3)實際上是在async2函數返回的Promise的then語句中執行的
async function async1() {
  console.log(1);
  const result = await async2();
  console.log(3);
}

async function async2() {
  console.log(2);
}
//console.log(async2())

Promise.resolve().then(() => {
  console.log(4);
});

setTimeout(() => {
  console.log(5);
});

async1();
console.log(6);

地獄模式: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。

相似循環...

因此最後輸出結果爲一、六、七、二、四、五、九、十、十一、八、3。

參考文章

相關文章
相關標籤/搜索