新聞中心
說到JavaScript的運(yùn)行原理,自然繞不開JS引擎,運(yùn)行上下文,單線程,事件循環(huán),事件驅(qū)動(dòng),回調(diào)函數(shù)等概念。本文主要參考文章[1,2]。
創(chuàng)新互聯(lián)2013年開創(chuàng)至今,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目做網(wǎng)站、網(wǎng)站設(shè)計(jì)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元秀山土家族苗族做網(wǎng)站,已為上家服務(wù),為秀山土家族苗族各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18980820575
為了更好的理解JavaScript如何工作的,首先要理解以下幾個(gè)概念。
JS Engine(JS引擎)
Runtime(運(yùn)行上下文)
Call Stack (調(diào)用棧)
Event Loop(事件循環(huán))
Callback (回調(diào))
1.JS Engine
簡(jiǎn)單來說,JS引擎主要是對(duì)JS代碼進(jìn)行詞法、語法等分析,通過編譯器將代碼編譯成可執(zhí)行的機(jī)器碼讓計(jì)算機(jī)去執(zhí)行。
目前最流行的JS引擎非V8莫屬了,Chrome瀏覽器和Node.js采用的引擎就是V8引擎。引擎的結(jié)構(gòu)可以簡(jiǎn)單由下圖表示:
就如JVM虛擬機(jī)一樣,JS引擎中也有堆(Memory Heap)和棧(Call Stack)的概念。
棧。用來存儲(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ǔ)在堆中。詳細(xì)可見此處1,3
關(guān)于閉包的情況,就涉及到Captured Variables。我們知道Local Variables是最簡(jiǎn)單的情形,是直接存儲(chǔ)在棧中的。而Captured Variables是對(duì)于存在閉包情況和with,try catch情況的變量。
function?foo?()?{?var?x;?//?local?variables ?var?y;?//?captured?variable,?bar中引用了y ?function?bar?()?{?//?bar?中的context會(huì)capture變量y ?use(y); ?}?return?bar; } 復(fù)制代碼
如上述情況,變量y存在與bar()的閉包中,因此y是captured variable,是存儲(chǔ)在堆中的。
2.RunTime
JS在瀏覽器中可以調(diào)用瀏覽器提供的API,如window對(duì)象,DOM相關(guān)API等。這些接口并不是由V8引擎提供的,是存在與瀏覽器當(dāng)中的。因此簡(jiǎn)單來說,對(duì)于這些相關(guān)的外部接口,可以在運(yùn)行時(shí)供JS調(diào)用,以及JS的事件循環(huán)(Event Loop)和事件隊(duì)列(Callback Queue),把這些稱為RunTime。有些地方也把JS所用到的core lib核心庫也看作RunTime的一部分。
同樣,在Node.js中,可以把Node的各種庫提供的API稱為RunTime。所以可以這么理解,Chrome和Node.js都采用相同的V8引擎,但擁有不同的運(yùn)行環(huán)境(RunTime Environments)[4]。
3.Call Stack
JS被設(shè)計(jì)為單線程運(yùn)行的,這是因?yàn)镴S主要用來實(shí)現(xiàn)很多交互相關(guān)的操作,如DOM相關(guān)操作,如果是多線程會(huì)造成復(fù)雜的同步問題。因此JS自誕生以來就是單線程的,而且主線程都是用來進(jìn)行界面相關(guān)的渲染操作?(為什么說是主線程,因?yàn)镠TML5 提供了Web Worker,獨(dú)立的一個(gè)后臺(tái)JS,用來處理一些耗時(shí)數(shù)據(jù)操作。因?yàn)椴粫?huì)修改相關(guān)DOM及頁面元素,因此不影響頁面性能),如果有阻塞產(chǎn)生會(huì)導(dǎo)致瀏覽器卡死。
如果一個(gè)遞歸調(diào)用沒有終止條件,是一個(gè)死循環(huán)的話,會(huì)導(dǎo)致調(diào)用棧內(nèi)存不夠而溢出,如:
function?foo()?{ ?foo(); }foo();復(fù)制代碼
例子中foo函數(shù)循環(huán)調(diào)用其本身,且沒有終止條件,瀏覽器控制臺(tái)輸出調(diào)用棧達(dá)到最大調(diào)用次數(shù)。
JS線程如果遇到比較耗時(shí)操作,如讀取文件,AJAX請(qǐng)求操作怎么辦?這里JS用到了Callback回調(diào)函數(shù)來處理。
對(duì)于Call Stack中的每個(gè)方法調(diào)用,都會(huì)形成它自己的一個(gè)執(zhí)行上下文Execution Context,關(guān)于執(zhí)行上下文的詳細(xì)闡述請(qǐng)看這篇文章
4.Event Loop & Callback
JS通過回調(diào)的方式,異步處理耗時(shí)的任務(wù)。一個(gè)簡(jiǎn)單的例子:
var?result?=?ajax('...'); console.log(result); 復(fù)制代碼
此時(shí)并不會(huì)得到result的值,result是undefined。這是因?yàn)閍jax的調(diào)用是異步的,當(dāng)前線程并不會(huì)等到ajax請(qǐng)求到結(jié)果后才執(zhí)行console.log語句。而是調(diào)用ajax后請(qǐng)求的操作交給回調(diào)函數(shù),自己是立刻返回。正確的寫法應(yīng)該是:
ajax('...',?function(result)?{?console.log(result); }) 復(fù)制代碼
此時(shí)才能正確輸出請(qǐng)求返回的結(jié)果。
JS引擎其實(shí)并不提供異步的支持,異步支持主要依賴于運(yùn)行環(huán)境(瀏覽器或Node.js)。
So, for example, when your JavaScript program makes an Ajax request to fetch some data from the server, you set up the “response” code in a function (the “callback”), and the JS Engine tells the hosting environment: “Hey, I’m going to suspend execution for now, but whenever you finish with that network request, and you have some data, please call this function back.”
The browser is then set up to listen for the response from the network, and when it has something to return to you, it will schedule the callback function to be executed by inserting it into the event loop.
上面這兩段話摘自于How JavaScript works,以通俗的方式解釋了JS如何調(diào)用回調(diào)函數(shù)實(shí)現(xiàn)異步處理。
所以什么是Event Loop?
Event Loop只做一件事情,負(fù)責(zé)監(jiān)聽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è)操作。
一個(gè)setTimeout的例子以及對(duì)應(yīng)的Event Loop動(dòng)態(tài)圖:
console.log('Hi'); setTimeout(function?cb1()?{? ?console.log('cb1'); },?5000);console.log('Bye'); 復(fù)制代碼
setTimeout有個(gè)要注意的地方,如上述例子延遲5s執(zhí)行,不是嚴(yán)格意義上的5s,正確來說是至少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í)行。
所以常見的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復(fù)制代碼
在說一個(gè)容易犯錯(cuò)的栗子:
for?(var?i?=?0;?i?5;?i++)?{ ?setTimeout(function()?{?console.log(i); ?},?1000?*?i); } //?輸出:5?5?5?5?5復(fù)制代碼
上面這個(gè)栗子并不是輸出0,1,2,3,4,第一反應(yīng)覺得應(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ì)列中,等for循環(huán)執(zhí)行完在依次取出放進(jìn)調(diào)用棧。當(dāng)for循環(huán)執(zhí)行完時(shí),i的值已經(jīng)變成5,所以最后輸出全都是5。
關(guān)于定時(shí)器又可以看看這篇有意思的文章
最后關(guān)于Event Loop,可以參考下這個(gè)視頻。到目前為止說的event loop是前端瀏覽器中的event loop,關(guān)于Nodejs的Event Loop的細(xì)節(jié)闡述,請(qǐng)看我的另一篇文章Node.js design pattern : Reactor (Event Loop)。兩者的區(qū)別對(duì)比可查看這篇文章你不知道的Event Loop,對(duì)兩種event loop做了相關(guān)總結(jié)和比較。
總結(jié)
最后總結(jié)一下,JS的運(yùn)行原理主要有以下幾個(gè)方面:
JS引擎主要負(fù)責(zé)把JS代碼轉(zhuǎn)為機(jī)器能執(zhí)行的機(jī)器碼,而JS代碼中調(diào)用的一些WEB API則由其運(yùn)行環(huán)境提供,這里指的是瀏覽器。
JS是單線程運(yùn)行,每次都從調(diào)用棧出取出代碼進(jìn)行調(diào)用。如果當(dāng)前代碼非常耗時(shí),則會(huì)阻塞當(dāng)前線程導(dǎo)致瀏覽器卡頓。
回調(diào)函數(shù)是通過加入到事件隊(duì)列中,等待Event Loop拿出并放到調(diào)用棧中進(jìn)行調(diào)用。只有Event Loop監(jiān)聽到調(diào)用棧為空時(shí),才會(huì)從事件隊(duì)列中從隊(duì)頭拿出回調(diào)函數(shù)放進(jìn)調(diào)用棧里。
名稱欄目:JavaScript運(yùn)行原理解析
本文鏈接:http://www.ef60e0e.cn/article/jgodoc.html