新聞中心
說到JavaScript的運行原理,自然繞不開JS引擎,運行上下文,單線程,事件循環(huán),事件驅動,回調函數(shù)等概念。

公司主營業(yè)務:網站設計制作、網站設計、移動網站開發(fā)等業(yè)務。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網宣傳,提高企業(yè)的競爭能力。創(chuàng)新互聯(lián)建站是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團隊。公司秉承以“開放、自由、嚴謹、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領域給我們帶來的挑戰(zhàn),讓我們激情的團隊有機會用頭腦與智慧不斷的給客戶帶來驚喜。創(chuàng)新互聯(lián)建站推出港北免費做網站回饋大家。
為了更好的理解JavaScript如何工作的,首先要理解以下幾個概念。
- JS Engine(JS引擎)
- Runtime(運行上下文)
- Call Stack (調用棧)
- Event Loop(事件循環(huán))
- Callback (回調)
1.JS Engine
簡單來說,JS引擎主要是對JS代碼進行詞法、語法等分析,通過編譯器將代碼編譯成可執(zhí)行的機器碼讓計算機去執(zhí)行。
目前最流行的JS引擎非V8莫屬了,Chrome瀏覽器和Node.js采用的引擎就是V8引擎。引擎的結構可以簡單由下圖表示:
[[284891]]
就如JVM虛擬機一樣,JS引擎中也有堆(Memory Heap)和棧(Call Stack)的概念。
- 棧。用來存儲方法調用的地方,以及基礎數(shù)據類型(如var a = 1)也是存儲在棧里面的,會隨著方法調用結束而自動銷毀掉(入棧-->方法調用后-->出棧)。
- 堆。JS引擎中給對象分配的內存空間是放在堆中的。如var foo = {name: 'foo'} 那么這個foo所指向的對象是存儲在堆中的。
此外,JS中存在閉包的概念,對于基本類型變量如果存在與閉包當中,那么也將存儲在堆中。詳細可見此處1,3
關于閉包的情況,就涉及到Captured Variables。我們知道Local Variables是最簡單的情形,是直接存儲在棧中的。而Captured Variables是對于存在閉包情況和with,try catch情況的變量。
- function foo () {
- var x; // local variables
- var y; // captured variable, bar中引用了y
- function bar () {
- // bar 中的context會capture變量y
- use(y);
- }
- return bar;
- }
如上述情況,變量y存在與bar()的閉包中,因此y是captured variable,是存儲在堆中的。
2.RunTime
JS在瀏覽器中可以調用瀏覽器提供的API,如window對象,DOM相關API等。這些接口并不是由V8引擎提供的,是存在與瀏覽器當中的。因此簡單來說,對于這些相關的外部接口,可以在運行時供JS調用,以及JS的事件循環(huán)(Event Loop)和事件隊列(Callback Queue),把這些稱為RunTime。有些地方也把JS所用到的core lib核心庫也看作RunTime的一部分。
同樣,在Node.js中,可以把Node的各種庫提供的API稱為RunTime。所以可以這么理解,Chrome和Node.js都采用相同的V8引擎,但擁有不同的運行環(huán)境(RunTime Environments)[4]。
3.Call Stack
JS被設計為單線程運行的,這是因為JS主要用來實現(xiàn)很多交互相關的操作,如DOM相關操作,如果是多線程會造成復雜的同步問題。因此JS自誕生以來就是單線程的,而且主線程都是用來進行界面相關的渲染操作 (為什么說是主線程,因為HTML5 提供了Web Worker,獨立的一個后臺JS,用來處理一些耗時數(shù)據操作。因為不會修改相關DOM及頁面元素,因此不影響頁面性能),如果有阻塞產生會導致瀏覽器卡死。
如果一個遞歸調用沒有終止條件,是一個死循環(huán)的話,會導致調用棧內存不夠而溢出,如:
- function foo() {
- foo();
- }
- foo();
例子中foo函數(shù)循環(huán)調用其本身,且沒有終止條件,瀏覽器控制臺輸出調用棧達到最大調用次數(shù)。
JS線程如果遇到比較耗時操作,如讀取文件,AJAX請求操作怎么辦?這里JS用到了Callback回調函數(shù)來處理。
對于Call Stack中的每個方法調用,都會形成它自己的一個執(zhí)行上下文Execution Context,關于執(zhí)行上下文的詳細闡述請看這篇文章
4.Event Loop & Callback
JS通過回調的方式,異步處理耗時的任務。一個簡單的例子:
var result = ajax('...');console.log(result);復制代碼
此時并不會得到result的值,result是undefined。這是因為ajax的調用是異步的,當前線程并不會等到ajax請求到結果后才執(zhí)行console.log語句。而是調用ajax后請求的操作交給回調函數(shù),自己是立刻返回。正確的寫法應該是:
- ajax('...', function(result) {
- console.log(result);
- })
此時才能正確輸出請求返回的結果。
JS引擎其實并不提供異步的支持,異步支持主要依賴于運行環(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如何調用回調函數(shù)實現(xiàn)異步處理。
所以什么是Event Loop?
Event Loop只做一件事情,負責監(jiān)聽Call Stack和Callback Queue。當Call Stack里面的調用棧運行完變成空了,Event Loop就把Callback Queue里面的第一條事件(其實就是回調函數(shù))放到調用棧中并執(zhí)行它,后續(xù)不斷循環(huán)執(zhí)行這個操作。
一個setTimeout的例子以及對應的Event Loop動態(tài)圖:
- console.log('Hi');
- setTimeout(function cb1() {
- console.log('cb1');
- }, 5000);
- console.log('Bye');
setTimeout有個要注意的地方,如上述例子延遲5s執(zhí)行,不是嚴格意義上的5s,正確來說是至少5s以后會執(zhí)行。因為Web API會設定一個5s的定時器,時間到期后將回調函數(shù)加到隊列中,此時該回調函數(shù)還不一定會馬上運行,因為隊列中可能還有之前加入的其他回調函數(shù),而且還必須等到Call Stack空了之后才會從隊列中取一個回調執(zhí)行。
所以常見的setTimeout(callback, 0) 的做法就是為了在常規(guī)的調用介紹后馬上運行回調函數(shù)。
- console.log('Hi');
- setTimeout(function() {
- console.log('callback');
- }, 0);
- console.log('Bye');
- // 輸出
- // Hi
- // Bye
- // callback
在說一個容易犯錯的栗子:
- for (var i = 0; i < 5; i++) {
- setTimeout(function() {
- console.log(i);
- }, 1000 * i);
- }
- // 輸出:5 5 5 5 5
上面這個栗子并不是輸出0,1,2,3,4,第一反應覺得應該是這樣。但梳理了JS的時間循環(huán)后,應該很容易明白。
調用棧先執(zhí)行 for(var i = 0; i < 5; i++) {...}方法,里面的定時器會到時間后會直接把回調函數(shù)放到事件隊列中,等for循環(huán)執(zhí)行完在依次取出放進調用棧。當for循環(huán)執(zhí)行完時,i的值已經變成5,所以最后輸出全都是5。
關于定時器又可以看看這篇有意思的文章
最后關于Event Loop,可以參考下這個視頻。到目前為止說的event loop是前端瀏覽器中的event loop,關于Nodejs的Event Loop的細節(jié)闡述,請看我的另一篇文章Node.js design pattern : Reactor (Event Loop)。兩者的區(qū)別對比可查看這篇文章你不知道的Event Loop,對兩種event loop做了相關總結和比較。
總結
最后總結一下,JS的運行原理主要有以下幾個方面:
- JS引擎主要負責把JS代碼轉為機器能執(zhí)行的機器碼,而JS代碼中調用的一些WEB API則由其運行環(huán)境提供,這里指的是瀏覽器。
- JS是單線程運行,每次都從調用棧出取出代碼進行調用。如果當前代碼非常耗時,則會阻塞當前線程導致瀏覽器卡頓。
- 回調函數(shù)是通過加入到事件隊列中,等待Event Loop拿出并放到調用棧中進行調用。只有Event Loop監(jiān)聽到調用棧為空時,才會從事件隊列中從隊頭拿出回調函數(shù)放進調用棧里。
網站欄目:JavaScript 運行原理解析
新聞來源:http://m.fisionsoft.com.cn/article/cocsphj.html


咨詢
建站咨詢
