摘要:字節(jié)碼不能直接運行,而是運行在一個虛擬機之上,一般也把虛擬機稱為引擎。這些事件排成隊列,等候進入主線程。執(zhí)行至完成每一個消息執(zhí)行完成后,其它消息才會被執(zhí)行。零延遲零延遲并不是意味著回調(diào)會立即執(zhí)行。
JavaScript虛擬機
JavaScript是一種解釋型語言,也就是說,它不需要編譯,可以由解釋器實時運行。這樣的好處是運行和修改都比較方便,刷新頁面就可以重新解釋;缺點是每次運行都要調(diào)用解釋器,系統(tǒng)開銷較大,運行速度慢于編譯型語言。為了提高運行速度,目前的瀏覽器都將JavaScript進行一定程度的編譯,生成類似字節(jié)碼(bytecode)的中間代碼,以提高運行速度。
早期,瀏覽器內(nèi)部對JavaScript的處理過程如下:
讀取代碼,進行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
對詞元進行語法分析(parsing),將代碼整理成“語法樹”(syntax tree)。
使用“翻譯器”(translator),將代碼轉(zhuǎn)為字節(jié)碼(bytecode)。
使用“字節(jié)碼解釋器”(bytecode interpreter),將字節(jié)碼轉(zhuǎn)為機器碼。
逐行解釋將字節(jié)碼轉(zhuǎn)為機器碼,是很低效的。為了提高運行速度,現(xiàn)代瀏覽器改為采用“即時編譯”(Just In Time compiler,縮寫JIT),即字節(jié)碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結(jié)果緩存(inline cache)。通常,一個程序被經(jīng)常用到的,只是其中一小部分代碼,有了緩存的編譯結(jié)果,整個程序的運行速度就會顯著提升。
不同的瀏覽器有不同的編譯策略。有的瀏覽器只編譯最經(jīng)常用到的部分,比如循環(huán)的部分;有的瀏覽器索性省略了字節(jié)碼的翻譯步驟,直接編譯成機器碼,比如chrome瀏覽器的V8引擎。
字節(jié)碼不能直接運行,而是運行在一個虛擬機(Virtual Machine)之上,一般也把虛擬機稱為JavaScript引擎。因為JavaScript運行時未必有字節(jié)碼,所以JavaScript虛擬機并不完全基于字節(jié)碼,而是部分基于源碼,即只要有可能,就通過JIT(just in time)編譯器直接把源碼編譯成機器碼運行,省略字節(jié)碼步驟。這一點與其他采用虛擬機(比如Java)的語言不盡相同。這樣做的目的,是為了盡可能地優(yōu)化代碼、提高性能。下面是目前最常見的一些JavaScript虛擬機:
Chakra)(Microsoft Internet Explorer)
Nitro/JavaScript Core (Safari)
Carakan (Opera)
SpiderMonkey (Firefox)
V8) (Chrome, Chromium)
單線程模型JavaScript采用單線程模型,也就是說,所有的任務(wù)都在一個線程里運行。這意味著,一次只能運行一個任務(wù),其他任務(wù)都必須在后面排隊等待。
JavaScript之所以采用單線程,而不是多線程,跟歷史有關(guān)系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果,對于一種網(wǎng)頁腳本語言來說,這就太復(fù)雜了。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準?所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質(zhì)。
單線程模型帶來了一些問題,主要是新的任務(wù)被加在隊列的尾部,只有前面的所有任務(wù)運行結(jié)束,才會輪到它執(zhí)行。如果有一個任務(wù)特別耗時,后面的任務(wù)都會停在那里等待,造成瀏覽器失去響應(yīng),又稱“假死”。為了避免“假死”,當某個操作在一定時間后仍無法結(jié)束,瀏覽器就會跳出提示框,詢問用戶是否要強行停止腳本運行。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來,再往下執(zhí)行。JavaScript語言的設(shè)計者意識到,這時CPU完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機制就是JavaScript內(nèi)部采用的Event Loop。
注:ajax同步異步可設(shè)置、用戶/瀏覽器自執(zhí)行事件(onclick、onload、onkeyup等等)以及定時器(setTimeout、setInterval)是異步操作。
Event Loop所謂Event Loop,指的是一種內(nèi)部循環(huán),用來排列和處理事件,以及執(zhí)行函數(shù)。Wikipedia的定義是:“Event Loop是一個程序結(jié)構(gòu),用于等待和發(fā)送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù);異步任務(wù)指的是,不進入主線程、而進入“任務(wù)隊列”(task queue)的任務(wù),只有“任務(wù)隊列”通知主線程,某個異步任務(wù)可以執(zhí)行了,該任務(wù)才會進入主線程執(zhí)行。
以Ajax操作為例,它可以當作同步任務(wù)處理,也可以當作異步任務(wù)處理,由開發(fā)者決定。如果是同步任務(wù),主線程就等著Ajax操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),該任務(wù)直接進入“任務(wù)隊列”,主線程跳過Ajax操作,直接往下執(zhí)行,等到Ajax操作有了結(jié)果,主線程再執(zhí)行對應(yīng)的回調(diào)函數(shù)。
想要理解Event Loop,就要從程序的運行模式講起。運行以后的程序叫做"進程"(process),一般情況下,一個進程一次只能執(zhí)行一個任務(wù)。如果有很多任務(wù)需要執(zhí)行,不外乎三種解決方法。
排隊。因為一個進程一次只能執(zhí)行一個任務(wù),只好等前面的任務(wù)執(zhí)行完了,再執(zhí)行后面的任務(wù)。
新建進程。使用fork命令,為每個任務(wù)新建一個進程。
新建線程。因為進程太耗費資源,所以如今的程序往往允許一個進程包含多個線程,由線程去完成任務(wù)。
如果某個任務(wù)很耗時,比如涉及很多I/O(輸入/輸出)操作,那么線程的運行大概是下面的樣子。
上圖的綠色部分是程序的運行時間,紅色部分是等待時間??梢钥吹?,由于I/O操作很慢,所以這個線程的大部分運行時間都在空等I/O操作的返回結(jié)果。這種運行方式稱為"同步模式"(synchronous I/O)。
如果采用多線程,同時運行多個任務(wù),那很可能就是下面這樣。
上圖表明,多線程不僅占用多倍的系統(tǒng)資源,也閑置多倍的資源,這顯然不合理。
上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閑時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應(yīng)的I/O程序,然后接著往后運行,所以不存在紅色的等待時間。等到I/O程序完成操作,Event Loop線程再把結(jié)果返回主線程。主線程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個任務(wù)。
可以看到,由于多出了橙色的空閑時間,所以主線程得以運行更多的任務(wù),這就提高了效率。這種運行方式稱為"異步模式"(asynchronous I/O)。
這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢。如果部署得好,JavaScript程序是不會出現(xiàn)堵塞的,這就是為什么node.js平臺可以用很少的資源,應(yīng)付大流量訪問的原因。
任務(wù)隊列如果有大量的異步任務(wù)(實際情況就是這樣),它們會在“任務(wù)隊列”中注冊大量的事件。這些事件排成隊列,等候進入主線程。本質(zhì)上,“任務(wù)隊列”就是一個事件“先進先出”的數(shù)據(jù)結(jié)構(gòu)。比如,點擊鼠標就產(chǎn)生一些列事件,mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。
事件循環(huán)之所以稱為 事件循環(huán),是因為它經(jīng)常被用于類似如下的方式來實現(xiàn):
while (queue.waitForMessage()) { queue.processNextMessage(); }
如果當前沒有任何消息,queue.waitForMessage 會等待著同步將要到來的消息。
"執(zhí)行至完成"每一個消息執(zhí)行完成后,其它消息才會被執(zhí)行。當你分析你的程序時,這點提供了一些優(yōu)秀的特性,包括每當一個函數(shù)運行時,它就不能被搶占,并且在其他代碼運行之前完全運行(且可以修改此函數(shù)控制的數(shù)據(jù))。這點與C語言不同。例如,C語言中當一個程序在一個線程中運行時,它可以在任何點停止且可以在其它線程中運行其它代碼。
這個模型的一個缺點在于當一個消息的完成耗時過長,網(wǎng)絡(luò)應(yīng)用無法處理用戶的交互如點擊或者滾動。瀏覽器用“程序需要過長時間運行”的對話框來緩解這個問題。一個比較好的解決方案是使消息處理變短且如果可能的話,將一個消息拆分成幾個消息。
添加消息在瀏覽器里,當一個事件出現(xiàn)且有一個事件監(jiān)聽器被綁定時,消息會被隨時添加。如果沒有事件監(jiān)聽器,事件會丟失。所以點擊一個附帶點擊事件處理函數(shù)的元素會添加一個消息。其它事件亦然。
調(diào)用 setTimeout 函數(shù)會在一個時間段過去后在隊列中添加一個消息。這個時間段作為函數(shù)的第二個參數(shù)被傳入。如果隊列中沒有其它消息,消息會被馬上處理。但是,如果有其它消息,setTimeout 消息必須等待其它消息處理完。因此第二個參數(shù)僅僅表示最少的時間 而非確切的時間。
零延遲零延遲 (Zero delay) 并不是意味著回調(diào)會立即執(zhí)行。在零延遲調(diào)用 setTimeout 時,其并不是過了給定的時間間隔后就馬上執(zhí)行回調(diào)函數(shù)。其等待的時間基于隊列里正在等待的消息數(shù)量。在下面的例子中,"this is just a message" 將會在回調(diào) (callback) 獲得處理之前輸出到控制臺,這是因為延遲是要求運行時 (runtime) 處理請求所需的最小時間,但不是有所保證的時間。
(function () { console.log("this is the start"); setTimeout(function cb() { console.log("this is a msg from call back"); }); console.log("this is just a message"); setTimeout(function cb1() { console.log("this is a msg from call back1"); }, 0); console.log("this is the end"); })(); // "this is the start" // "this is just a message" // "this is the end" // "this is a msg from call back" // "this is a msg from call back1"多個運行時互相通信
一個 web worker 或者一個跨域的 iframe 都有它們自己的棧,堆和消息隊列。兩個不同的運行時只有通過 postMessage 方法進行通信。這個方法會給另一個運行時添加一個消息如果后者監(jiān)聽了 message 事件。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/88669.html
摘要:當函數(shù)結(jié)束,將會被從調(diào)用棧移出。事件循環(huán)事件循環(huán)的責任就是查看調(diào)用棧并確定調(diào)用棧是否為空。事件循環(huán)會再次檢查調(diào)用棧是否為空,如果為空的話,它會把事件回調(diào)壓入棧中,然后回調(diào)函數(shù)則被執(zhí)行。 寫在文章前 這篇文章是翻譯自Sukhjinder Arora的Understanding Asynchronous JavaScript。這篇文章描述了異步和同步JavaScript是如何在運行環(huán)境中,...
showImg(https://segmentfault.com/img/bVbjYU7?w=2000&h=1333); 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! JavsScript 是一門單線程的編程語言,這就意味著一個時間里只能處理一件事,也就是說 JavaScript 引擎一次只能在一個線程里處理一條語句。 雖然單線程簡化了編程代碼,因為你不必太擔心并發(fā)引出的問...
摘要:異步回調(diào)被作為實參傳入另一函數(shù),并在該外部函數(shù)內(nèi)被調(diào)用,用以來完成某些任務(wù)的函數(shù),稱為回調(diào)函數(shù)?;卣{(diào)函數(shù)經(jīng)常被用于繼續(xù)執(zhí)行一個異步完成后的操作,它們被稱為異步回調(diào)?;卣{(diào)函數(shù)是事件循環(huán)回頭調(diào)用到程序中的目標,隊列處理到這個項目的時候會運行它。 唯一比不知道代碼為什么崩潰更可怕的事情是,不知道為什么一開始它是工作的! 在 ECMA 規(guī)范的最近幾次版本里不斷有新成員加入,尤其在處理異步的問題...
摘要:創(chuàng)建全局上下文由表示,并將全局上下文推到棧頂。在了解異步執(zhí)行之前還需要知道一些概念,事件循環(huán)和回調(diào)隊列也稱為任務(wù)隊列或消息隊列。會等待事件循環(huán)調(diào)度。事件循環(huán)事件循環(huán)的作用是查看調(diào)用棧并確定調(diào)用棧是否空閑。 簡評:如果你對 JavaScript 異步的原理感興趣,這里有一篇不錯的介紹。 JavaScript 同步代碼是如果工作的 在介紹 JavaScript 異步執(zhí)行之前先來了解一下, ...
摘要:異步如果在函數(shù)返回的時候,調(diào)用者還不能購得到預(yù)期結(jié)果,而是將來通過一定的手段得到例如回調(diào)函數(shù),這就是異步。的意思是,將回調(diào)函數(shù)立刻插入消息隊列,等待執(zhí)行,而不是立即執(zhí)行。 大家好,我是wmingren,小伙伴們都知道JavaScript是單線程的語言,所謂的單線程呢就是指如果有多個任務(wù)就必須去排隊,前面任務(wù)執(zhí)行完成后,后面任務(wù)再執(zhí)行。到這里我們就產(chǎn)生了一個疑問,既然是單線程的,又怎么會...
閱讀 1755·2021-11-18 13:20
閱讀 1140·2021-10-11 10:59
閱讀 2985·2021-08-24 10:01
閱讀 3499·2019-08-29 14:21
閱讀 3350·2019-08-29 14:15
閱讀 3512·2019-08-26 12:23
閱讀 3341·2019-08-26 11:46
閱讀 3344·2019-08-26 11:35