「譯」理解Javascript函數(shù)執(zhí)行—調(diào)用棧叠萍、事件循環(huán)芝发、任務等

現(xiàn)如今,web開發(fā)者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情苛谷,從提供瀏覽器中的交互辅鲸,到開發(fā)電腦游戲、桌面工具腹殿、跨平臺移動應用独悴,甚至可以在服務端部署(如最流行的Node.js)來連結任意數(shù)據(jù)庫例书。因此,了解Javascript的內(nèi)部構造很重要刻炒,這樣才能更優(yōu)更高效的使用它决采。這也是本文的主旨所在。

Javascript的生態(tài)正在變得越來越復雜坟奥。要構建一個現(xiàn)代web應用树瞭,會不可避免的用到Webpack、Babel爱谁、ESLint晒喷、Mocha、Karma访敌、Grunt……我該用哪個凉敲?這些都是干嘛的?我找到了這個漫畫寺旺,它完美詮釋了如今的web開發(fā)者的水深火熱:

image

Javascript疲勞癥——學習Javascript是什么感覺

在一頭扎進框架和庫的海洋之前爷抓,每個Javascript開發(fā)者首先需要了解Javascript在底層是如何實現(xiàn)的。差不多每個JS開發(fā)者都聽過“V8”這個術語迅涮,但有些人可能根本不知道這個詞到底什么意思废赞、干嘛用的。在我職業(yè)開發(fā)生涯的第一年里叮姑,我對這些花里胡哨的術語所知甚少唉地,我更關心先完成工作。但這樣并不能滿足我的好奇心传透,我好奇Javascript是他喵的怎么能做到這一切的耘沼。我決定要深挖一番,我翻遍Google朱盐,找到一些優(yōu)秀的博客群嗤,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經(jīng)驗并分享出來兵琳。鑒于有太多東西要了解狂秘,我把本文分為兩個部分。這一部分會介紹常用術語躯肌,第二部分則會闡述這些術語之間的關聯(lián)者春。

Javascript是一個單線程單并發(fā)的語言,也就是說它一次只能處理一個任務清女,執(zhí)行一條代碼钱烟。它的調(diào)用棧連同堆、隊列一起構成了Javascript并發(fā)模型(在V8中實現(xiàn))。讓我們一個個地看這幾個詞拴袭。

image

Visual Representation of JS Model

  1. 調(diào)用棧(Call Stack):它是記錄我們在程序中調(diào)用函數(shù)的數(shù)據(jù)結構读第。假如我們調(diào)用一個函數(shù)來執(zhí)行,就是在把某種記錄推入到調(diào)用棧的頂端拥刻;當我們從一個函數(shù)中返回出來怜瞒,就從調(diào)用棧頂端彈出記錄。
image

JS Stack Visualization

當我們運行上圖中的代碼般哼,我們會先尋找所有執(zhí)行的開端——主函數(shù)盼砍。在上例中,一系列執(zhí)行開始于console.log(bar(6))逝她,那么這一次執(zhí)行就被推入調(diào)用棧中浇坐,它上面一層就是函數(shù)bar及其參數(shù),函數(shù)bar轉(zhuǎn)而調(diào)用函數(shù)foo黔宛,foo也被推入棧中近刘;而foo隨即return了某個值,所以被彈出調(diào)用棧臀晃;類似地觉渴,bar隨后彈出,最后console語句打印了結果并彈出徽惋。所有這些舉動都依次發(fā)生在須臾之間案淋。

你們肯定都在瀏覽器控制臺見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式险绘,簡單表明了調(diào)用棧的當前狀態(tài)以及在函數(shù)中何處報錯(見下圖)踢京。

image

Error stack trace

有時候,當我們以遞歸的形式多次調(diào)用一個函數(shù)宦棺,就會陷入無限循環(huán)中瓣距,而對于Chrome瀏覽器來說,它對調(diào)用棧的大小的限制是16000層代咸,超出限制就會終止程序并拋出達到棧上限錯誤(見下圖)蹈丸。

image
  1. :對象會被分配到堆——內(nèi)存中的松散結構。所有的針對變量和對象的內(nèi)存分配都在堆中進行呐芥。
  2. 隊列:一種Javascript運行時逻杖,包含了一個消息隊列,這個隊列就是一系列將被處理的信息和要執(zhí)行的相關回調(diào)函數(shù)思瘟。當調(diào)用棧有足夠空間荸百,就從隊列中取出一條消息并進行處理,該消息調(diào)用相關聯(lián)的函數(shù)(并因此產(chǎn)生一個初始化棧層)潮太。當棧再次清空時管搪,消息處理也就結束了。簡單說铡买,這些消息被排成隊列更鲁,指定回調(diào)函數(shù)來響應外部異步事件(例如鼠標點擊或HTTP請求的響應)。諸如用戶點擊按鈕而沒有相應回調(diào)函數(shù)的情況奇钞,就不會有消息放入隊列中澡为。

事件循環(huán)(event loop)

當我們評估JS代碼的性能時,要知道調(diào)用棧中的函數(shù)會讓程序或快或慢景埃,console.log()會很快媒至,但用for或while迭代成千上萬次就會慢一些,并且讓調(diào)用棧一直被占用被阻塞著谷徙。這就叫做阻塞腳本拒啰,你可能在Webpage Speed Insights中見過。

網(wǎng)絡請求會慢完慧,圖片請求會慢谋旦,但萬幸,服務請求可以通過AJAX這種異步函數(shù)完成屈尼。假如那些網(wǎng)絡請求用同步函數(shù)來完成册着,將會如何?網(wǎng)絡請求發(fā)送到服務器——服務器也就是某處的某種機器罷了脾歧,現(xiàn)在假設服務器返回響應可能會緩慢甲捏,此時,如果我點擊一些CTA(call-to-action)按鈕鞭执,或者其他一些需要完成的渲染司顿,就不會有什么反應,因為調(diào)用棧還被之前的網(wǎng)絡請求阻塞著兄纺。在Ruby等多線程語言中免猾,這種情況可以控制,但像Javascript這種單線程語言囤热,除非調(diào)用棧中的函數(shù)返回值猎提,否則就一直堵著。瀏覽器沒有任何反應旁蔼,網(wǎng)頁就會崩潰锨苏。這樣我們可沒辦法為最終用戶提供流暢的用戶界面。那我們怎么辦棺聊?

“JS中的并發(fā)——一次只做一件事伞租,異步回調(diào)除外”

最早的解決方案就是用異步回調(diào),這意味著我們給某部分代碼加一個回調(diào)限佩,該回調(diào)會在這段代碼執(zhí)行完成后執(zhí)行葵诈。我們肯定都遇到過諸如AJAX請求用的$.get()裸弦、setTimeout()、setInterval()作喘、Promises的異步回調(diào)理疙。Node都是基于異步函數(shù)執(zhí)行的。所有那些異步回調(diào)不會像console.log()等同步函數(shù)那樣立刻運行泞坦,而是在之后的某個時刻運行窖贤,所以不會立刻就推到調(diào)用棧中去。那它們到底去哪里了贰锁?怎么控制它們赃梧?

image

如上例,若一個網(wǎng)絡請求在Javascript中運行:

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

1\. 請求函數(shù)被執(zhí)行豌熄,給`onreadystatechange`事件傳一個匿名函數(shù)作為回調(diào)授嘀,用來在將來響應就緒的時候執(zhí)行。
2\. “Script call done锣险!”立刻輸出到控制臺粤攒。
3\. 后續(xù)某時刻,響應被返回囱持,回調(diào)被執(zhí)行夯接,響應體被輸出到控制臺。

</pre>

在等待異步操作完成并解除回調(diào)執(zhí)行之時纷妆,響應的解耦調(diào)用允許Javascript運行時做別的事盔几。瀏覽器插入進來調(diào)用了它的API,這是用C++實現(xiàn)的API掩幢,用來創(chuàng)建線程以控制諸如DOM事件逊拍、http請求、setTimeout等異步事件际邻。

那些web接口不能自己把執(zhí)行代碼推入調(diào)用棧芯丧,如果能,那么該接口會隨機出現(xiàn)在你的代碼中(執(zhí)行順序不可控)世曾。上面討論過的消息回調(diào)隊列說明了這一點缨恒。任何web接口在執(zhí)行完畢后,都會把回調(diào)推入這個隊列轮听。事件循環(huán)此時就要負責控制隊列中的回調(diào)的執(zhí)行骗露,并在棧空時把回調(diào)推入棧中血巍。事件循環(huán)的基本工作就是監(jiān)聽調(diào)用棧和任務隊列萧锉,當它看到棧空了述寡,就把隊列中第一個任務推入棧柿隙。每個消息或者回調(diào)都在上一個任務處理完再開始處理叶洞。

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

while (queue.waitForMessage()) {
 queue.processNextMessage();
}

</pre>

image

Javascript Event Loop Visual Representation

在web瀏覽器中,一旦某事件發(fā)生并綁定了事件監(jiān)聽器禀崖,消息就立即添加到隊列中衩辟。如果沒有監(jiān)聽器,那就意味著事件丟失了帆焕。因此點擊一個綁定了點擊事件處理器,就會新增一個消息不恭,其他事件亦如此叶雹。對其回調(diào)的調(diào)用將會是調(diào)用棧中的初始層,而由于Javascript是單線程的换吧,在調(diào)用棧中所有調(diào)用都return之前折晦,后續(xù)的消息的輪詢和處理就暫停了。之后的(同步的)函數(shù)調(diào)用會向調(diào)用棧中增加新的調(diào)用層沾瓦。

在下一部分满着,我會通過一個動畫來展示上述過程的代碼執(zhí)行,深入解釋什么是不同類型的異步函數(shù)贯莺、隊列中誰優(yōu)先執(zhí)行风喇,以及諸如零延遲等功能的技巧。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缕探,一起剝皮案震驚了整個濱河市魂莫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爹耗,老刑警劉巖耙考,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潭兽,居然都是意外死亡倦始,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門山卦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞋邑,“玉大人,你說我怎么就攤上這事账蓉§庞” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵剔猿,是天一觀的道長视译。 經(jīng)常有香客問我,道長归敬,這世上最難降的妖魔是什么酷含? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任鄙早,我火速辦了婚禮,結果婚禮上椅亚,老公的妹妹穿的比我還像新娘限番。我一直安慰自己,他們只是感情好呀舔,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布弥虐。 她就那樣靜靜地躺著,像睡著了一般媚赖。 火紅的嫁衣襯著肌膚如雪霜瘪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天惧磺,我揣著相機與錄音颖对,去河邊找鬼。 笑死磨隘,一個胖子當著我的面吹牛缤底,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播番捂,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼个唧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了设预?” 一聲冷哼從身側(cè)響起坑鱼,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎絮缅,沒想到半個月后鲁沥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡耕魄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年画恰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吸奴。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡允扇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出则奥,到底是詐尸還是另有隱情考润,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布读处,位于F島的核電站糊治,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏罚舱。R本人自食惡果不足惜井辜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一绎谦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粥脚,春花似錦窃肠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窖壕。三九已至操禀,卻和暖如春乓诽,著一層夾襖步出監(jiān)牢的瞬間萧朝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工胖腾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓土砂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谜洽。 傳聞我的和親對象是個殘疾皇子萝映,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容