js 運(yùn)行原理解析

關(guān)于JavaScript 如何工作的,由以下這幾個(gè)概念

  • JS Engline (JS引擎)
  • Runtime(運(yùn)行上下文)
  • Call Stack (調(diào)用棧)
  • Event Loop(事件循環(huán))
  • Callback (回調(diào))

1. JS Engine

目前最流行的JS引擎非V8莫屬了,Chrome瀏覽器和Node.js采用的引擎就是V8引擎。
目前最流行的JS引擎非V8莫屬了,Chrome瀏覽器和Node.js采用的引擎就是V8引擎。引擎的結(jié)構(gòu)可以簡(jiǎn)單由下圖表示:
[圖片上傳失敗...(image-e2ed5b-1577415600424)]
就如JVM虛擬機(jī)一樣,JS引擎中也有堆(Memory Heap)和棧(Call Stack)的概念。

  • 棧。用來(lái)存儲(chǔ)方法調(diào)用的地方,以及基礎(chǔ)數(shù)據(jù)類型(如var a = 1)也是存儲(chǔ)在棧里面的,會(huì)隨著方法調(diào)用結(jié)束而自動(dòng)銷毀掉(入棧-->方法調(diào)用后-->出棧)。
  • 堆。JS引擎中給對(duì)象分配的內(nèi)存空間是放在堆中的。如var foo = {name: 'foo'} 那么這個(gè)foo所指向的對(duì)象是存儲(chǔ)在堆中的。

此外,JS中存在閉包的概念,對(duì)于基本類型變量如果存在與閉包當(dāng)中,那么也將存儲(chǔ)在堆中。

function foo () {
  var x; // local variables
  var y; // captured variable, bar中引用了y

  function bar () {
  // bar 中的context會(huì)capture變量y
    use(y);
  }

  return bar;
}

如上述情況,變量y存在與bar()的閉包中,因此y是captured variable,是存儲(chǔ)在堆中的。

2. RunTime

JS在瀏覽器中可以調(diào)用瀏覽器提供的API,如window對(duì)象,DOM相關(guān)API等。這些接口并不是由V8引擎提供的,是存在與瀏覽器當(dāng)中的。因此簡(jiǎn)單來(lái)說(shuō),對(duì)于這些相關(guān)的外部接口,可以在運(yùn)行時(shí)供JS調(diào)用,以及JS的事件循環(huán)(Event Loop)和事件隊(duì)列(Callback Queue),把這些稱為RunTime。有些地方也把JS所用到的core lib核心庫(kù)也看作RunTime的一部分。
[圖片上傳失敗...(image-d4fd49-1577415600424)]
同樣,在Node.js中,可以把Node的各種庫(kù)提供的API稱為RunTime。所以可以這么理解,Chrome和Node.js都采用相同的V8引擎,但擁有不同的運(yùn)行環(huán)境

3. Call Stack

JS被設(shè)計(jì)為單線程運(yùn)行的,這是因?yàn)镴S主要用來(lái)實(shí)現(xiàn)很多交互相關(guān)的操作,如DOM相關(guān)操作,如果是多線程會(huì)造成復(fù)雜的同步問(wèn)題。因此JS自誕生以來(lái)就是單線程的,而且主線程都是用來(lái)進(jìn)行界面相關(guān)的渲染操作
為什么說(shuō)是主線程,因?yàn)镠TML5 提供了Web Worker,獨(dú)立的一個(gè)后臺(tái)JS,用來(lái)處理一些耗時(shí)數(shù)據(jù)操作。因?yàn)椴粫?huì)修改相關(guān)DOM及頁(yè)面元素,因此不影響頁(yè)面性能
如果有阻塞產(chǎn)生會(huì)導(dǎo)致瀏覽器卡死。

如果一個(gè)遞歸調(diào)用沒(méi)有終止條件,是一個(gè)死循環(huán)的話,會(huì)導(dǎo)致調(diào)用棧內(nèi)存不夠而溢出,如:

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

例子中foo函數(shù)循環(huán)調(diào)用其本身,且沒(méi)有終止條件,瀏覽器控制臺(tái)輸出調(diào)用棧達(dá)到最大調(diào)用次數(shù)。

[圖片上傳失敗...(image-9c7535-1577415600424)]

JS線程如果遇到比較耗時(shí)操作,如讀取文件,AJAX請(qǐng)求操作怎么辦?這里JS用到了Callback回調(diào)函數(shù)來(lái)處理。

對(duì)于Call Stack中的每個(gè)方法調(diào)用,都會(huì)形成它自己的一個(gè)執(zhí)行上下文Execution Context

4.Event Loop & Callback

JS通過(guò)回調(diào)的方式,異步處理耗時(shí)的任務(wù)。一個(gè)簡(jiǎn)單的例子:

var result = ajax('...');
console.log(result);

此時(shí)并不會(huì)得到result的值,result是undefined。這是因?yàn)閍jax的調(diào)用是異步的,當(dāng)前線程并不會(huì)等到ajax請(qǐng)求到結(jié)果后才執(zhí)行console.log語(yǔ)句。而是調(diào)用ajax后請(qǐng)求的操作交給回調(diào)函數(shù),自己是立刻返回。正確的寫(xiě)法應(yīng)該是:

ajax('...', function(result) {
    console.log(result);
})

此時(shí)才能正確輸出請(qǐng)求返回的結(jié)果。

JS引擎其實(shí)并不提供異步的支持,異步支持主要依賴于運(yùn)行環(huán)境(瀏覽器或Node.js)。

所以什么是Event Loop?

Event Loop只做一件事情,負(fù)責(zé)監(jiān)聽(tīng)Call Stack和Callback Queue。當(dāng)Call Stack里面的調(diào)用棧運(yùn)行完變成空了,Event Loop就把Callback Queue里面的第一條事件(其實(shí)就是回調(diào)函數(shù))放到調(diào)用棧中并執(zhí)行它,后續(xù)不斷循環(huán)執(zhí)行這個(gè)操作。

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');
image

setTimeout有個(gè)要注意的地方,如上述例子延遲5s執(zhí)行,不是嚴(yán)格意義上的5s,正確來(lái)說(shuō)是至少5s以后會(huì)執(zhí)行。因?yàn)閃eb API會(huì)設(shè)定一個(gè)5s的定時(shí)器,時(shí)間到期后將回調(diào)函數(shù)加到隊(duì)列中,此時(shí)該回調(diào)函數(shù)還不一定會(huì)馬上運(yùn)行,因?yàn)殛?duì)列中可能還有之前加入的其他回調(diào)函數(shù),而且還必須等到Call Stack空了之后才會(huì)從隊(duì)列中取一個(gè)回調(diào)執(zhí)行。

所以常見(jiàn)的setTimeout(callback, 0) 的做法就是為了在常規(guī)的調(diào)用介紹后馬上運(yùn)行回調(diào)函數(shù)

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');
// 輸出
// Hi
// Bye
// callback

在說(shuō)一個(gè)容易犯錯(cuò)的栗子:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i);
}
    
// 輸出:5 5 5 5 5

上面這個(gè)例子并不是輸出0,1,2,3,4,第一反應(yīng)覺(jué)得應(yīng)該是這樣。但梳理了JS的時(shí)間循環(huán)后,應(yīng)該很容易明白。

調(diào)用棧先執(zhí)行 ==for(var i = 0; i < 5; i++) {...}== 方法,里面的定時(shí)器會(huì)到時(shí)間后會(huì)直接把回調(diào)函數(shù)放到事件隊(duì)列中,等f(wàn)or循環(huán)執(zhí)行完在依次取出放進(jìn)調(diào)用棧。當(dāng)for循環(huán)執(zhí)行完時(shí),i的值已經(jīng)變成5,所以最后輸出全都是5

總結(jié)

  • JS引擎主要負(fù)責(zé)把JS代碼轉(zhuǎn)為機(jī)器能執(zhí)行的機(jī)器碼,而JS代碼中調(diào)用的一些WEB API則由運(yùn)行環(huán)境提供,這里代指瀏覽器。
  • JS是單線程,每次都從調(diào)用棧取出代碼進(jìn)行掉用。如果當(dāng)前代碼非常耗時(shí),則會(huì)阻塞當(dāng)前線程瀏覽器導(dǎo)致瀏覽器卡頓。
  • 回調(diào)函數(shù)是通過(guò)加入到事件隊(duì)列,等待Event Loop拿出并放到調(diào)用棧中進(jìn)行調(diào)用。只有Event Loop監(jiān)聽(tīng)到調(diào)用棧(Call Stack)為空時(shí),才會(huì)從事件隊(duì)列中從隊(duì)頭拿出回調(diào)函數(shù)放入調(diào)用棧里。
最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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