Javascript是如何運(yùn)行的?(1)引擎、調(diào)用棧、事件循環(huán)

寫在前面

從公眾號(hào)建號(hào)至今,發(fā)了不少技術(shù)文,基本上每篇都有提到基礎(chǔ)的重要性。

不久前我發(fā)了個(gè)朋友圈,內(nèi)容是這樣的「學(xué)習(xí),應(yīng)該先學(xué)習(xí)更好的思維,而不是更多的知識(shí),在一個(gè)落后的思維模式里,增加再多的信息量,也只是低水平的重復(fù)?!?/p>

我向來強(qiáng)調(diào)基礎(chǔ)知識(shí)的重要性,基礎(chǔ)打牢了,框架,真的非常容易學(xué)

好了,不啰嗦了,步入正題

作為JavaScript使用人員,V8引擎作為一個(gè)概念,我想大多數(shù)人都聽說過,而且絕大多數(shù)人也知道JavaScript是一個(gè)單線程語言,或者知道JavaScript使用的是回調(diào)隊(duì)列的形式

在這篇文章中,我們來詳細(xì)解釋這些概念,從而來解釋JavaScript是如何運(yùn)行的

希望詳細(xì)了解了這些內(nèi)容后,你可以寫出更好的非阻塞的代碼,并且可以正確的使用JavaScript的API

JavaScript引擎

Google的V8引擎是現(xiàn)在最流行的JavaScript引擎,以Chrome為代表的瀏覽器,還有Node.js都是使用的V8引擎。

那什么是JavaScript引擎呢?

JavaScript引擎是執(zhí)行JavaScript代碼的程序或解釋器。 JavaScript引擎可以實(shí)現(xiàn)為標(biāo)準(zhǔn)解釋器,或即時(shí)編譯器,它以某種形式將JavaScript編譯為字節(jié)碼。

咱們不糾結(jié)于這個(gè)概念,糾結(jié)概念不是咱們應(yīng)該做的事情。但是概念必須熟記,因?yàn)楦拍钍情L篇大論的論證后得出的精髓。后面的文章中咱們?cè)冁告傅纴?/p>

下面主要簡單描述下JavaScript引擎

一個(gè)非常簡單的視圖,看看引擎包含了什么:



V8引擎由兩個(gè)主要的組件組成

  • Memory Heap(內(nèi)存堆)--內(nèi)存分配的任務(wù)就在這里面完成
  • Call Stack(調(diào)用棧)--這是代碼執(zhí)行時(shí)堆棧調(diào)用的位置

JavaScript運(yùn)行時(shí)

幾乎所有的JavaScript開發(fā)人員都使用過瀏覽器中的API(例如:setTimeout)。

其實(shí)這些API都不是由引擎提供的。

那么,這些API是從哪里來的呢?是由宿主提供的。

宿主是指JavaScript的運(yùn)行環(huán)境。運(yùn)行在瀏覽器上那么宿主就是指瀏覽器,運(yùn)行在Node.js上宿主就是Node

就像前端界大牛winter說的那樣,我們應(yīng)該形成感性的認(rèn)知:一個(gè)JavaScript引擎會(huì)常駐在內(nèi)存中,它等待著宿主把JavaScript代碼或者函數(shù)傳遞給它執(zhí)行

那宿主把JavaScript傳遞給引擎后,引擎是怎么處理的呢?這就牽扯到調(diào)用棧與循環(huán)隊(duì)列了,繼續(xù)往下看,一個(gè)一個(gè)的說

調(diào)用棧(Call Stack)

先來說說調(diào)用棧

JavaScript是一個(gè)單線程語言,這意味著它只有一個(gè)堆棧,因此,它每次只能完成一件事情。

這個(gè)堆棧調(diào)用,是一種數(shù)據(jù)結(jié)構(gòu),它記錄了程序中的位置,如果我們進(jìn)入函數(shù),如果我們進(jìn)入一個(gè)函數(shù),就將這個(gè)函數(shù)放在這個(gè)棧的頂部,如果我們從函數(shù)返回(return),那么就將這個(gè)函數(shù)從棧的頂部彈出。

我們現(xiàn)在給引擎一串JavaScript代碼,看看引擎是怎么執(zhí)行我們的代碼的

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

當(dāng)JavaScript引擎開始執(zhí)行這個(gè)代碼時(shí),先會(huì)清空棧,棧將會(huì)如下圖執(zhí)行:

從圖中Step1,我們可以看出,我們之前定義的函數(shù)在沒有使用的時(shí)候,并沒有進(jìn)入棧中。所以未被調(diào)用的函數(shù)并不會(huì)存在棧中。

繼續(xù)看Step1,我們使用了printSquare,那么就要進(jìn)入這個(gè)函數(shù)進(jìn)去看看它里面到底實(shí)現(xiàn)了什么。此時(shí)我們就將我們進(jìn)入的這個(gè)函數(shù)放到棧的最上方(雖然只有它自己但我們也要這樣描述)

我們進(jìn)入這個(gè)函數(shù)后,發(fā)現(xiàn)這貨還調(diào)用了別的函數(shù)multiply,沒辦法,只好再進(jìn)入multiply,也就到了Step2,將multiply放入棧的最頂部

執(zhí)行完multiply后,將它從棧的頂部丟出去,繼續(xù)console。log(s),也就可以看到是Step3

以此類推,直到調(diào)用棧清空。JavaScript引擎則將我們的代碼執(zhí)行完畢。

上圖棧中的每一步,均被稱為堆棧幀(Stack Frame),每一幀都代表著棧的變動(dòng)

我們?cè)賮砜纯?,?dāng)拋出錯(cuò)誤時(shí),堆棧如何構(gòu)造跟蹤的

當(dāng)異常發(fā)生時(shí),基本上是調(diào)用堆棧的狀態(tài)

function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();

在Chrome瀏覽器中執(zhí)行上述代碼,將產(chǎn)生以下堆棧跟蹤

這個(gè)錯(cuò)誤信息怎么看呢?從上往下看,這就是一個(gè)調(diào)用棧的內(nèi)容,現(xiàn)在處于頂部的函數(shù)拋出異常,無法正常的從棧頂部彈出

再來說說堆棧溢出,當(dāng)達(dá)到最大調(diào)用棧大小時(shí),就非常容易發(fā)生「堆棧溢出」

function foo() {
    foo();
}
foo();

當(dāng)JavaScript引擎開始執(zhí)行上述代碼時(shí),它開始調(diào)用函數(shù)foo,并且這個(gè)函數(shù)還會(huì)自己調(diào)用自己,還沒有任何終止條件。

所以,在執(zhí)行的每個(gè)步驟中,函數(shù)會(huì)一遍又一遍的添加到調(diào)用棧中,就像下圖這樣:


但是,有些時(shí)候,調(diào)用堆棧中的函數(shù)調(diào)用超過調(diào)用堆棧的實(shí)際大小時(shí),瀏覽器會(huì)采取措施,拋出錯(cuò)誤,如下圖這樣:

在單線程上運(yùn)行代碼非常簡單,因?yàn)椴槐乜紤]多線程環(huán)境中出現(xiàn)的復(fù)雜場景,比如死鎖、讀寫一致等

但是在單個(gè)線程上運(yùn)行也是非常有限的,由于JavaScript只有一個(gè)調(diào)用棧,當(dāng)調(diào)用的某個(gè)函數(shù),執(zhí)行的非常緩慢時(shí),我們又該怎么辦呢?

并發(fā)與事件循環(huán)

如果在調(diào)用堆棧中有的函數(shù)需要花費(fèi)大量的時(shí)間才能處理時(shí),那后面的內(nèi)容不就卡死了么?

比如說在JavaScript中進(jìn)行一些復(fù)雜的圖像處理。問題就在調(diào)用棧在執(zhí)行這個(gè)圖像處理函數(shù)時(shí),它是無法再做任何別的事情的。

這意味著瀏覽器無法渲染,無法運(yùn)行任何其它的代碼,看起來它就像是卡住了

并且這還不是唯一的問題,一旦瀏覽器在調(diào)用棧中開始處理大量的任務(wù),瀏覽器可能就會(huì)停止響應(yīng),并且大量的瀏覽器會(huì)報(bào)錯(cuò),告訴你當(dāng)前頁面崩潰了

網(wǎng)頁都崩掉了,還有用戶體驗(yàn)可言么?

如果想網(wǎng)頁流程,那么就需要避免此類問題。

那么,我們?nèi)绾卧诓蛔枞鸘I并使瀏覽器無響應(yīng)的情況下執(zhí)行繁重的代碼呢?這依靠的就是JavaScript的「異步回調(diào)」

這時(shí)候就牽扯到JavaScript引擎的異步事件循環(huán)

在這兒,我覺得GitHub上用戶「@Mavericker-1996」的回答已經(jīng)說的非常詳細(xì)到位,在此我就不重復(fù)造輪子了,只是略加修改

任務(wù)隊(duì)列

首先我們需要明白以下幾件事情:

  • JS分為同步任務(wù)和異步任務(wù)
  • 同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)調(diào)用棧
  • 主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放置一個(gè)事件
  • 一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑),系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行

根據(jù)規(guī)范,事件循環(huán)是通過任務(wù)隊(duì)列的機(jī)制來進(jìn)行協(xié)調(diào)的。

一個(gè) Event Loop 中,可以有一個(gè)或者多個(gè)任務(wù)隊(duì)列(task queue)。

一個(gè)任務(wù)隊(duì)列便是一系列有序任務(wù)(task)的集合,每個(gè)任務(wù)都有一個(gè)任務(wù)源(task source),源自同一個(gè)任務(wù)源的 task 必須放到同一個(gè)任務(wù)隊(duì)列,從不同源來的則被添加到不同隊(duì)列。 setTimeout/Promise 等API便是任務(wù)源,而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)。

宏任務(wù)

(macro)task(又稱之為宏任務(wù)),可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)。

瀏覽器為了能夠使得JS內(nèi)部(macro)task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)(macro)task執(zhí)行結(jié)束后,在下一個(gè)(macro)task 執(zhí)行開始前,對(duì)頁面進(jìn)行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

(macro)task主要包含:script(整體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 環(huán)境)

微任務(wù)

microtask(又稱為微任務(wù)),可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。也就是說,在當(dāng)前task任務(wù)后,下一個(gè)task之前,在渲染之前。

所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會(huì)更快,因?yàn)闊o需等渲染。也就是說,在某一個(gè)macrotask執(zhí)行完后,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)。

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 環(huán)境)

運(yùn)行機(jī)制

在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱為 tick,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的,但關(guān)鍵步驟如下:

  • 執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲取)
  • 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
  • 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
  • 當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
  • 渲染完畢后,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)

流程圖如下:

Promise和async中的立即執(zhí)行

我們知道Promise中的異步體現(xiàn)在thencatch中,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的。而在async/await中,在出現(xiàn)await出現(xiàn)之前,其中的代碼也是立即執(zhí)行的。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢?

await做了什么?

從字面意思上看await就是等待,await 等待的是一個(gè)表達(dá)式,這個(gè)表達(dá)式的返回值可以是一個(gè)promise對(duì)象也可以是其他值。

很多人以為await會(huì)一直等待之后的表達(dá)式執(zhí)行完之后才會(huì)繼續(xù)執(zhí)行后面的代碼。

實(shí)際上await是一個(gè)讓出線程的標(biāo)志。await后面的表達(dá)式會(huì)先執(zhí)行一遍,將await后面的代碼加入到microtask中,然后就會(huì)跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼。

由于因?yàn)閍sync await 本身就是promise+generator的語法糖。所以await后面的代碼是microtask。所以對(duì)于本題中的

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等價(jià)于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

寫在最后

了解原理,了解執(zhí)行機(jī)制不像學(xué)習(xí)框架的使用那樣簡單,是一件略微困難的事情。

因?yàn)榭蚣芏紝⑦@些內(nèi)容給封裝起來了,并不需要我們?nèi)ミM(jìn)行處理,但是就算我們使用框架,也需要了解執(zhí)行機(jī)制,這樣我們書寫的代碼邏輯順序才不會(huì)出錯(cuò),得到的結(jié)果才能與預(yù)期一致。

基于JavaScript的框架,均是建立在此基礎(chǔ)之上

本文牽扯內(nèi)容點(diǎn)較多,有些內(nèi)容一筆帶過了,但并不代表不重要,后面我會(huì)逐一細(xì)寫。

如果對(duì)后面的并發(fā)與事件循環(huán)覺得內(nèi)容較難理解,可以先看看我之前寫的白話入門篇:6分鐘看懂Node.js武功精髓

關(guān)注微信公眾號(hào)「鬧鬧吃魚」更多有趣的內(nèi)容等著你哦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容